• 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.subtle;
18 
19 import com.google.crypto.tink.AccessesPartialKey;
20 import com.google.crypto.tink.InsecureSecretKeyAccess;
21 import com.google.crypto.tink.StreamingAead;
22 import com.google.crypto.tink.streamingaead.AesGcmHkdfStreamingKey;
23 import com.google.crypto.tink.streamingaead.AesGcmHkdfStreamingParameters.HashType;
24 import java.nio.ByteBuffer;
25 import java.nio.ByteOrder;
26 import java.security.GeneralSecurityException;
27 import java.security.InvalidAlgorithmParameterException;
28 import java.util.Arrays;
29 import javax.crypto.Cipher;
30 import javax.crypto.spec.GCMParameterSpec;
31 import javax.crypto.spec.SecretKeySpec;
32 
33 /**
34  * Streaming encryption using {@code AES-GCM} with {@code HKDF} as key derivation function.
35  *
36  * <p>Each ciphertext uses a new AES-GCM key that is derived from the key derivation key, a randomly
37  * chosen salt of the same size as the key and a nonce prefix.
38  *
39  * <p>The format of a ciphertext is header || segment_0 || segment_1 || ... || segment_k. The header
40  * has size this.getHeaderLength(). Its format is headerLength || salt || prefix. where headerLength
41  * is 1 byte determining the size of the header, salt is a salt used in the key derivation and
42  * prefix is the prefix of the nonce. In principle headerLength is redundant information, since the
43  * length of the header can be determined from the key size.
44  *
45  * <p>segment_i is the i-th segment of the ciphertext. The size of segment_1 .. segment_{k-1} is
46  * ciphertextSegmentSize. segment_0 is shorter, so that segment_0, the header and other information
47  * of size firstSegmentOffset align with ciphertextSegmentSize.
48  *
49  * <p>This class does not work on Android KitKat (API level 19) or older.
50  *
51  * @since 1.1.0
52  */
53 @AccessesPartialKey
54 public final class AesGcmHkdfStreaming extends NonceBasedStreamingAead {
55   // TODO(bleichen): Some things that are not yet decided:
56   //   - What can we assume about the state of objects after getting an exception?
57   //   - Should there be a simple test to detect invalid ciphertext offsets?
58   //   - Should we encode the size of the header?
59   //   - Should we encode the size of the segments?
60   //   - Should a version be included in the header to allow header modification?
61   //   - Should we allow other information, header and the first segment the to be a multiple of
62   //     ciphertextSegmentSize?
63   //   - This implementation fixes a number of parameters. Should there be more options?
64   //   - Should we authenticate ciphertextSegmentSize and firstSegmentSize?
65   //     If an attacker can change these parameters then this would allow to move
66   //     the position of plaintext in the file. While these parameters are currently
67   //     specified by the key it is unclear whether this will remain so in the future.
68   //
69   // The size of the IVs for GCM
70   private static final int NONCE_SIZE_IN_BYTES = 12;
71 
72   // The nonce has the format nonce_prefix || ctr || last_block
73   // The nonce_prefix is constant for the whole file.
74   // The ctr is a 32 bit ctr, the last_block is 1 if this is the
75   // last block of the file and 0 otherwise.
76   private static final int NONCE_PREFIX_IN_BYTES = 7;
77 
78   // The size of the tags of each ciphertext segment.
79   private static final int TAG_SIZE_IN_BYTES = 16;
80 
81   private final int keySizeInBytes;
82   private final int ciphertextSegmentSize;
83   private final int plaintextSegmentSize;
84   private final int firstSegmentOffset;
85   private final String hkdfAlg;
86   private final byte[] ikm;
87 
88   /**
89    * Initializes a streaming primitive with a key derivation key and encryption parameters.
90    *
91    * @param ikm input keying material used to derive sub keys.
92    * @param hkdfAlg the JCE MAC algorithm name, e.g., HmacSha256, used for the HKDF key derivation.
93    * @param keySizeInBytes the key size of the sub keys
94    * @param ciphertextSegmentSize the size of ciphertext segments.
95    * @param firstSegmentOffset the offset of the first ciphertext segment. That means the first
96    *     segment has size ciphertextSegmentSize - getHeaderLength() - firstSegmentOffset
97    * @throws InvalidAlgorithmParameterException if ikm is too short, the key size not supported or
98    *     ciphertextSegmentSize is to short.
99    */
AesGcmHkdfStreaming(byte[] ikm, String hkdfAlg, int keySizeInBytes, int ciphertextSegmentSize, int firstSegmentOffset)100   public AesGcmHkdfStreaming(byte[] ikm, String hkdfAlg, int keySizeInBytes,
101       int ciphertextSegmentSize, int firstSegmentOffset)
102       throws InvalidAlgorithmParameterException {
103     if (ikm.length < 16 || ikm.length < keySizeInBytes) {
104       throw new InvalidAlgorithmParameterException(
105           "ikm too short, must be >= " + Math.max(16, keySizeInBytes));
106     }
107     Validators.validateAesKeySize(keySizeInBytes);
108     if (ciphertextSegmentSize <= firstSegmentOffset + getHeaderLength() + TAG_SIZE_IN_BYTES) {
109       throw new InvalidAlgorithmParameterException("ciphertextSegmentSize too small");
110     }
111     this.ikm = Arrays.copyOf(ikm, ikm.length);
112     this.hkdfAlg = hkdfAlg;
113     this.keySizeInBytes = keySizeInBytes;
114     this.ciphertextSegmentSize = ciphertextSegmentSize;
115     this.firstSegmentOffset = firstSegmentOffset;
116     this.plaintextSegmentSize = ciphertextSegmentSize - TAG_SIZE_IN_BYTES;
117   }
118 
AesGcmHkdfStreaming(AesGcmHkdfStreamingKey key)119   private AesGcmHkdfStreaming(AesGcmHkdfStreamingKey key)
120       throws GeneralSecurityException {
121     this.ikm = key.getInitialKeyMaterial().toByteArray(InsecureSecretKeyAccess.get());
122     String hkdfAlgString = "";
123     if (key.getParameters().getHkdfHashType().equals(HashType.SHA1)) {
124       hkdfAlgString = "HmacSha1";
125     } else if (key.getParameters().getHkdfHashType().equals(HashType.SHA256)) {
126       hkdfAlgString = "HmacSha256";
127     } else if (key.getParameters().getHkdfHashType().equals(HashType.SHA512)) {
128       hkdfAlgString = "HmacSha512";
129     } else {
130       throw new GeneralSecurityException(
131           "Unknown HKDF algorithm " + key.getParameters().getHkdfHashType());
132     }
133     this.hkdfAlg = hkdfAlgString;
134     this.keySizeInBytes = key.getParameters().getDerivedAesGcmKeySizeBytes();
135     this.ciphertextSegmentSize = key.getParameters().getCiphertextSegmentSizeBytes();
136     // Current KeyTypeManager always sets this to 0.
137     this.firstSegmentOffset = 0;
138     this.plaintextSegmentSize = this.ciphertextSegmentSize - TAG_SIZE_IN_BYTES;
139   }
140 
create(AesGcmHkdfStreamingKey key)141   public static StreamingAead create(AesGcmHkdfStreamingKey key)
142       throws GeneralSecurityException {
143     return new AesGcmHkdfStreaming(key);
144   }
145 
146   @Override
newStreamSegmentEncrypter(byte[] aad)147   public AesGcmHkdfStreamEncrypter newStreamSegmentEncrypter(byte[] aad)
148       throws GeneralSecurityException {
149     return new AesGcmHkdfStreamEncrypter(aad);
150   }
151 
152   @Override
newStreamSegmentDecrypter()153   public AesGcmHkdfStreamDecrypter newStreamSegmentDecrypter() throws GeneralSecurityException {
154     return new AesGcmHkdfStreamDecrypter();
155   }
156 
157   @Override
getPlaintextSegmentSize()158   public int getPlaintextSegmentSize() {
159     return plaintextSegmentSize;
160   }
161 
162   @Override
getCiphertextSegmentSize()163   public int getCiphertextSegmentSize() {
164     return ciphertextSegmentSize;
165   }
166 
167   @Override
getHeaderLength()168   public int getHeaderLength() {
169     return 1 + keySizeInBytes + NONCE_PREFIX_IN_BYTES;
170   }
171 
172   @Override
getCiphertextOffset()173   public int getCiphertextOffset() {
174     return getHeaderLength() + firstSegmentOffset;
175   }
176 
177   @Override
getCiphertextOverhead()178   public int getCiphertextOverhead() {
179     return TAG_SIZE_IN_BYTES;
180   }
181 
getFirstSegmentOffset()182   public int getFirstSegmentOffset() {
183     return firstSegmentOffset;
184   }
185 
186   /**
187    * Returns the expected size of the ciphertext for a given plaintext. The returned value includes
188    * the header and offset.
189    */
expectedCiphertextSize(long plaintextSize)190   public long expectedCiphertextSize(long plaintextSize) {
191     long offset = getCiphertextOffset();
192     long fullSegments = (plaintextSize + offset) / plaintextSegmentSize;
193     long ciphertextSize = fullSegments * ciphertextSegmentSize;
194     long lastSegmentSize = (plaintextSize + offset) % plaintextSegmentSize;
195     if (lastSegmentSize > 0) {
196       ciphertextSize += lastSegmentSize + TAG_SIZE_IN_BYTES;
197     }
198     return ciphertextSize;
199   }
200 
cipherInstance()201   private static Cipher cipherInstance() throws GeneralSecurityException {
202     return EngineFactory.CIPHER.getInstance("AES/GCM/NoPadding");
203   }
204 
randomSalt()205   private byte[] randomSalt() {
206     return Random.randBytes(keySizeInBytes);
207   }
208 
paramsForSegment(byte[] prefix, long segmentNr, boolean last)209   private static GCMParameterSpec paramsForSegment(byte[] prefix, long segmentNr, boolean last)
210       throws GeneralSecurityException {
211     ByteBuffer nonce = ByteBuffer.allocate(NONCE_SIZE_IN_BYTES);
212     nonce.order(ByteOrder.BIG_ENDIAN);
213     nonce.put(prefix);
214     SubtleUtil.putAsUnsigedInt(nonce, segmentNr);
215     nonce.put((byte) (last ? 1 : 0));
216     return new GCMParameterSpec(8 * TAG_SIZE_IN_BYTES, nonce.array());
217   }
218 
randomNonce()219   private static byte[] randomNonce() {
220     return Random.randBytes(NONCE_PREFIX_IN_BYTES);
221   }
222 
deriveKeySpec(byte[] salt, byte[] aad)223   private SecretKeySpec deriveKeySpec(byte[] salt, byte[] aad) throws GeneralSecurityException {
224     byte[] key = Hkdf.computeHkdf(hkdfAlg, ikm, salt, aad, keySizeInBytes);
225     return new SecretKeySpec(key, "AES");
226   }
227 
228   /**
229    * An instance of a crypter used to encrypt a plaintext stream. The instances have state:
230    * encryptedSegments counts the number of encrypted segments. This state is used to generate the
231    * IV for each segment. By enforcing that only the method encryptSegment can increment this state,
232    * we can guarantee that the IV does not repeat.
233    */
234   class AesGcmHkdfStreamEncrypter implements StreamSegmentEncrypter {
235     private final SecretKeySpec keySpec;
236     private final Cipher cipher;
237     private final byte[] noncePrefix;
238     private final ByteBuffer header;
239     private long encryptedSegments = 0;
240 
AesGcmHkdfStreamEncrypter(byte[] aad)241     public AesGcmHkdfStreamEncrypter(byte[] aad) throws GeneralSecurityException {
242       cipher = cipherInstance();
243       encryptedSegments = 0;
244       byte[] salt = randomSalt();
245       noncePrefix = randomNonce();
246       header = ByteBuffer.allocate(getHeaderLength());
247       header.put((byte) getHeaderLength());
248       header.put(salt);
249       header.put(noncePrefix);
250       header.flip();
251       keySpec = deriveKeySpec(salt, aad);
252     }
253 
254     @Override
getHeader()255     public ByteBuffer getHeader() {
256       return header.asReadOnlyBuffer();
257     }
258 
259     /**
260      * Encrypts the next plaintext segment. This uses encryptedSegments as the segment number for
261      * the encryption.
262      */
263     @Override
encryptSegment( ByteBuffer plaintext, boolean isLastSegment, ByteBuffer ciphertext)264     public synchronized void encryptSegment(
265         ByteBuffer plaintext, boolean isLastSegment, ByteBuffer ciphertext)
266         throws GeneralSecurityException {
267       cipher.init(
268           Cipher.ENCRYPT_MODE,
269           keySpec,
270           paramsForSegment(noncePrefix, encryptedSegments, isLastSegment));
271       encryptedSegments++;
272       cipher.doFinal(plaintext, ciphertext);
273     }
274 
275     /**
276      * Encrypt a segment consisting of two parts. This method simplifies the case where one part of
277      * the plaintext is buffered and the other part is passed in by the caller.
278      */
279     @Override
encryptSegment( ByteBuffer part1, ByteBuffer part2, boolean isLastSegment, ByteBuffer ciphertext)280     public synchronized void encryptSegment(
281         ByteBuffer part1, ByteBuffer part2, boolean isLastSegment, ByteBuffer ciphertext)
282         throws GeneralSecurityException {
283       cipher.init(
284           Cipher.ENCRYPT_MODE,
285           keySpec,
286           paramsForSegment(noncePrefix, encryptedSegments, isLastSegment));
287       encryptedSegments++;
288       // `update(nonEmpty)`, `doFinal(empty)` is known to cause problems on Android 23.
289       // See https://github.com/google/tink/issues/229
290       if (part2.hasRemaining()) {
291         cipher.update(part1, ciphertext);
292         cipher.doFinal(part2, ciphertext);
293       } else {
294         cipher.doFinal(part1, ciphertext);
295       }
296     }
297   }
298 
299   /** An instance of a crypter used to decrypt a ciphertext stream. */
300   class AesGcmHkdfStreamDecrypter implements StreamSegmentDecrypter {
301     private SecretKeySpec keySpec;
302     private Cipher cipher;
303     private byte[] noncePrefix;
304 
AesGcmHkdfStreamDecrypter()305     AesGcmHkdfStreamDecrypter() {};
306 
307     @Override
init(ByteBuffer header, byte[] aad)308     public synchronized void init(ByteBuffer header, byte[] aad) throws GeneralSecurityException {
309       if (header.remaining() != getHeaderLength()) {
310         throw new InvalidAlgorithmParameterException("Invalid header length");
311       }
312       byte firstByte = header.get();
313       if (firstByte != getHeaderLength()) {
314         // We expect the first byte to be the length of the header.
315         // If this is not the case then either the ciphertext is incorrectly
316         // aligned or invalid.
317         throw new GeneralSecurityException("Invalid ciphertext");
318       }
319       noncePrefix = new byte[NONCE_PREFIX_IN_BYTES];
320       byte[] salt = new byte[keySizeInBytes];
321       header.get(salt);
322       header.get(noncePrefix);
323       keySpec = deriveKeySpec(salt, aad);
324       cipher = cipherInstance();
325     }
326 
327     @Override
decryptSegment( ByteBuffer ciphertext, int segmentNr, boolean isLastSegment, ByteBuffer plaintext)328     public synchronized void decryptSegment(
329         ByteBuffer ciphertext, int segmentNr, boolean isLastSegment, ByteBuffer plaintext)
330         throws GeneralSecurityException {
331       GCMParameterSpec params = paramsForSegment(noncePrefix, segmentNr, isLastSegment);
332       cipher.init(Cipher.DECRYPT_MODE, keySpec, params);
333       cipher.doFinal(ciphertext, plaintext);
334     }
335   }
336 }
337