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 java.io.BufferedInputStream; 21 import java.io.IOException; 22 import java.io.InputStream; 23 import java.security.GeneralSecurityException; 24 import java.util.List; 25 import javax.annotation.concurrent.GuardedBy; 26 27 /** 28 * A decrypter for ciphertext given in a {@link InputStream}. 29 */ 30 final class InputStreamDecrypter extends InputStream { 31 @GuardedBy("this") 32 boolean attemptedMatching; 33 @GuardedBy("this") 34 InputStream matchingStream; 35 @GuardedBy("this") 36 InputStream ciphertextStream; 37 38 List<StreamingAead> primitives; 39 byte[] associatedData; 40 41 /** 42 * Constructs a new decrypter for {@code ciphertextStream}. 43 * 44 * <p>The decrypter picks a matching {@code StreamingAead}-primitive from {@code primitives}, and 45 * uses it for decryption. The matching happens as follows: upon first {@code read()}-call each 46 * candidate primitive reads an initial portion of the stream, until it can determine whether the 47 * stream matches the key of the primitive. If a canditate does not match, then the stream is 48 * reset to its initial position, and the next candiate can attempt matching. The first successful 49 * candidate is then used exclusively on subsequent {@code read()}-calls. 50 * 51 * <p>The matching process wraps {@code ciphertextStream} into a BufferedInputStream, unless 52 * ciphertextStream supports rewinding (i.e. ciphertextStream.markSupported() == true). Buffering 53 * of the ciphertext is disabled once a ciphertext block has been successfully decrypted. 54 * 55 * @param primitives a list of possible {@link StreamingAeads} to try. Must not be mutateted by 56 * the caller. Will not be muteted by the {@code InputStreamDecrypter}, 57 * @param ciphertextStream the stream with the ciphertext 58 * @param associatedData The assocated data with this encryption 59 */ InputStreamDecrypter( List<StreamingAead> primitives, InputStream ciphertextStream, final byte[] associatedData)60 public InputStreamDecrypter( 61 List<StreamingAead> primitives, InputStream ciphertextStream, final byte[] associatedData) { 62 this.attemptedMatching = false; 63 this.matchingStream = null; 64 this.primitives = primitives; 65 // This class can use ciphertextStream directly if it supports mark and reset. 66 if (ciphertextStream.markSupported()) { 67 this.ciphertextStream = ciphertextStream; 68 } else { 69 this.ciphertextStream = new BufferedInputStream(ciphertextStream); 70 } 71 // We don't know how much ciphertext we have to cache. 72 // Fortunately, BufferedInputStream only allocates memory when needed, starting 73 // with an 8 kB buffer. 74 // If this strategy fails, then we should add a method to the StreamingPrimitives 75 // that returns the size of the ciphertext needed to decide if a key is valid. 76 this.ciphertextStream.mark(Integer.MAX_VALUE); 77 this.associatedData = associatedData.clone(); 78 } 79 80 /** 81 * Rewinds the ciphetext stream to the beginning of the ciphertext. 82 */ 83 @GuardedBy("this") rewind()84 private void rewind() throws IOException { 85 ciphertextStream.reset(); 86 } 87 88 /** 89 * Disable rewinding. 90 * This method is called once this class has found the correct key version. 91 * TODO(bleichen): While BufferedInputStream stops buffering new bytes, 92 * it does not shrink the intenal buffer. 93 */ 94 @GuardedBy("this") disableRewinding()95 private void disableRewinding() throws IOException { 96 ciphertextStream.mark(0); 97 } 98 99 /** 100 * This class does not support mark() and reset(). 101 * Applications that need random access to the plaintext of a long ciphertext 102 * should use a SeekableByteChannelDecrypter (or SeekableDecryptingChannel). 103 */ 104 @Override markSupported()105 public boolean markSupported() { 106 return false; 107 } 108 109 @Override 110 @GuardedBy("this") available()111 public synchronized int available() throws IOException { 112 if (matchingStream == null) { 113 return 0; 114 } else { 115 return matchingStream.available(); 116 } 117 } 118 119 @Override 120 @GuardedBy("this") read()121 public synchronized int read() throws IOException { 122 byte[] oneByte = new byte[1]; 123 if (read(oneByte) == 1) { 124 return oneByte[0] & 0xff; 125 } 126 return -1; 127 } 128 129 @Override 130 @GuardedBy("this") read(byte[] b)131 public synchronized int read(byte[] b) throws IOException { 132 return read(b, 0, b.length); 133 } 134 135 @Override 136 @GuardedBy("this") read(byte[] b, int offset, int len)137 public synchronized int read(byte[] b, int offset, int len) throws IOException { 138 if (len == 0) { 139 return 0; 140 } 141 if (matchingStream != null) { 142 return matchingStream.read(b, offset, len); 143 } else { 144 if (attemptedMatching) { 145 throw new IOException("No matching key found for the ciphertext in the stream."); 146 } 147 attemptedMatching = true; 148 for (StreamingAead streamingAead : primitives) { 149 try { 150 InputStream attemptedStream = 151 streamingAead.newDecryptingStream(ciphertextStream, associatedData); 152 int retValue = attemptedStream.read(b, offset, len); 153 if (retValue == 0) { 154 // Read should never return 0 when len > 0. 155 throw new IOException("Could not read bytes from the ciphertext stream"); 156 } 157 // Found a matching stream. 158 // If retValue > 0 then the first ciphertext segment has been decrypted and 159 // authenticated. If retValue == -1 then plaintext is empty and again this has been 160 // authenticated. 161 matchingStream = attemptedStream; 162 disableRewinding(); 163 return retValue; 164 } catch (IOException e) { 165 // Try another key. 166 // IOException is thrown e.g. when MAC is incorrect, but also in case 167 // of I/O failures. 168 // TODO(b/66098906): Use a subclass of IOException. 169 rewind(); 170 continue; 171 } catch (GeneralSecurityException e) { 172 // Try another key. 173 rewind(); 174 continue; 175 } 176 } 177 throw new IOException("No matching key found for the ciphertext in the stream."); 178 } 179 } 180 181 @Override 182 @GuardedBy("this") close()183 public synchronized void close() throws IOException { 184 ciphertextStream.close(); 185 } 186 } 187