• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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