1 // Copyright 2017 Google Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 //////////////////////////////////////////////////////////////////////////////// 16 17 package com.google.crypto.tink.streamingaead; 18 19 import com.google.crypto.tink.StreamingAead; 20 import com.google.crypto.tink.subtle.RewindableReadableByteChannel; 21 import java.io.IOException; 22 import java.nio.ByteBuffer; 23 import java.nio.channels.ReadableByteChannel; 24 import java.security.GeneralSecurityException; 25 import java.util.ArrayDeque; 26 import java.util.Deque; 27 import java.util.List; 28 import javax.annotation.concurrent.GuardedBy; 29 30 /** 31 * A decrypter for ciphertext given in a {@link ReadableByteChannel}. 32 */ 33 final class ReadableByteChannelDecrypter implements ReadableByteChannel { 34 @GuardedBy("this") 35 ReadableByteChannel attemptingChannel; 36 @GuardedBy("this") 37 ReadableByteChannel matchingChannel; 38 @GuardedBy("this") 39 RewindableReadableByteChannel ciphertextChannel; 40 41 // The StreamingAeads that have not yet been tried in nextAttemptingChannel. 42 Deque<StreamingAead> remainingPrimitives; 43 byte[] associatedData; 44 45 /** 46 * Constructs a new decrypter for {@code ciphertextChannel}. 47 * 48 * <p>The decrypter picks a matching {@code StreamingAead}-primitive from {@code primitives}, and 49 * uses it for decryption. The matching happens as follows: upon first {@code read()}-call each 50 * candidate primitive reads an initial portion of the channel, until it can determine whether the 51 * channel matches the key of the primitive. If a canditate does not match, then the channel is 52 * reset to its initial position, and the next candiate can attempt matching. The first successful 53 * candidate is then used exclusively on subsequent {@code read()}-calls. 54 * 55 * <p>The matching process uses a buffering wrapper around {@code ciphertextChannel} to enable 56 * resetting of the channel to the initial position. The buffering is removed once the matching is 57 * successful. 58 */ ReadableByteChannelDecrypter( List<StreamingAead> allPrimitives, ReadableByteChannel ciphertextChannel, final byte[] associatedData)59 public ReadableByteChannelDecrypter( 60 List<StreamingAead> allPrimitives, 61 ReadableByteChannel ciphertextChannel, 62 final byte[] associatedData) { 63 // There are 3 phases: 64 // 1) both matchingChannel and attemptingChannel are null. Rewind is enabled. 65 // 2) attemptingChannel is non-null, matchingChannel is null. Rewind is enabled. 66 // 3) attemptingChannel is null, matchingChannel is non-null. Rewind is disabled. 67 this.attemptingChannel = null; 68 this.matchingChannel = null; 69 this.remainingPrimitives = new ArrayDeque<>(); 70 for (StreamingAead primitive : allPrimitives) { 71 this.remainingPrimitives.add(primitive); 72 } 73 this.ciphertextChannel = new RewindableReadableByteChannel(ciphertextChannel); 74 this.associatedData = associatedData.clone(); 75 } 76 77 @GuardedBy("this") nextAttemptingChannel()78 private synchronized ReadableByteChannel nextAttemptingChannel() throws IOException { 79 while (!remainingPrimitives.isEmpty()) { 80 StreamingAead streamingAead = this.remainingPrimitives.removeFirst(); 81 try { 82 ReadableByteChannel decChannel = streamingAead.newDecryptingChannel( 83 ciphertextChannel, associatedData); 84 return decChannel; 85 } catch (GeneralSecurityException e) { 86 // Try another primitive. 87 ciphertextChannel.rewind(); 88 } 89 } 90 throw new IOException("No matching key found for the ciphertext in the stream."); 91 } 92 93 @Override read(ByteBuffer dst)94 public synchronized int read(ByteBuffer dst) throws IOException { 95 if (dst.remaining() == 0) { 96 return 0; 97 } 98 if (matchingChannel != null) { 99 return matchingChannel.read(dst); 100 } else { 101 if (attemptingChannel == null) { 102 attemptingChannel = nextAttemptingChannel(); 103 } 104 while (true) { 105 try { 106 int retValue = attemptingChannel.read(dst); 107 if (retValue == 0) { 108 // No data at the moment. Not clear if decryption was successful. 109 // Try again with the same stream next time. 110 return 0; 111 } 112 // Found a matching channel. 113 matchingChannel = attemptingChannel; 114 attemptingChannel = null; 115 ciphertextChannel.disableRewinding(); 116 return retValue; 117 } catch (IOException e) { 118 // Try another key. 119 // IOException is thrown e.g. when MAC is incorrect, but also in case 120 // of I/O failures. 121 ciphertextChannel.rewind(); 122 attemptingChannel = nextAttemptingChannel(); 123 } 124 } 125 } 126 } 127 128 @Override close()129 public synchronized void close() throws IOException { 130 ciphertextChannel.close(); 131 } 132 133 @Override isOpen()134 public synchronized boolean isOpen() { 135 return ciphertextChannel.isOpen(); 136 } 137 } 138