blob: 59eda8aa925fc9558a52a03c6cfeef3aadedd4bf [file] [log] [blame]
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package com.google.crypto.tink.subtle;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Streaming encryption using {@code AES-GCM} with {@code HKDF} as key derivation function.
*
* <p>Each ciphertext uses a new AES-GCM key that is derived from the key derivation key, a randomly
* chosen salt of the same size as the key and a nonce prefix.
*
* <p>The format of a ciphertext is header || segment_0 || segment_1 || ... || segment_k. The
* header has size this.getHeaderLength(). Its format is headerLength || salt || prefix. where
* headerLength is 1 byte determining the size of the header, salt is a salt used in the key
* derivation and prefix is the prefix of the nonce. In principle headerLength is redundant
* information, since the length of the header can be determined from the key size.
*
* <p>segment_i is the i-th segment of the ciphertext. The size of segment_1 .. segment_{k-1} is
* ciphertextSegmentSize. segment_0 is shorter, so that segment_0, the header and other information
* of size firstSegmentOffset align with ciphertextSegmentSize.
*
* <p>This class does not work on Android KitKat (API level 19) or older.
*
* @since 1.1.0
*/
public final class AesGcmHkdfStreaming extends NonceBasedStreamingAead {
// TODO(bleichen): Some things that are not yet decided:
// - What can we assume about the state of objects after getting an exception?
// - Should there be a simple test to detect invalid ciphertext offsets?
// - Should we encode the size of the header?
// - Should we encode the size of the segments?
// - Should a version be included in the header to allow header modification?
// - Should we allow other information, header and the first segment the to be a multiple of
// ciphertextSegmentSize?
// - This implementation fixes a number of parameters. Should there be more options?
// - Should we authenticate ciphertextSegmentSize and firstSegmentSize?
// If an attacker can change these parameters then this would allow to move
// the position of plaintext in the file. While these parameters are currently
// specified by the key it is unclear whether this will remain so in the future.
//
// The size of the IVs for GCM
private static final int NONCE_SIZE_IN_BYTES = 12;
// The nonce has the format nonce_prefix || ctr || last_block
// The nonce_prefix is constant for the whole file.
// The ctr is a 32 bit ctr, the last_block is 1 if this is the
// last block of the file and 0 otherwise.
private static final int NONCE_PREFIX_IN_BYTES = 7;
// The size of the tags of each ciphertext segment.
private static final int TAG_SIZE_IN_BYTES = 16;
private final int keySizeInBytes;
private final int ciphertextSegmentSize;
private final int plaintextSegmentSize;
private final int firstSegmentOffset;
private final String hkdfAlg;
private final byte[] ikm;
/**
* Initializes a streaming primitive with a key derivation key and encryption parameters.
*
* @param ikm input keying material used to derive sub keys.
* @param hkdfAlg the JCE MAC algorithm name, e.g., HmacSha256, used for the HKDF key derivation.
* @param keySizeInBytes the key size of the sub keys
* @param ciphertextSegmentSize the size of ciphertext segments.
* @param firstSegmentOffset the offset of the first ciphertext segment. That means the first
* segment has size ciphertextSegmentSize - getHeaderLength() - firstSegmentOffset
* @throws InvalidAlgorithmParameterException if ikm is too short, the key size not supported or
* ciphertextSegmentSize is to short.
*/
public AesGcmHkdfStreaming(byte[] ikm, String hkdfAlg, int keySizeInBytes,
int ciphertextSegmentSize, int firstSegmentOffset)
throws InvalidAlgorithmParameterException {
if (ikm.length < 16 || ikm.length < keySizeInBytes) {
throw new InvalidAlgorithmParameterException(
"ikm too short, must be >= " + Math.max(16, keySizeInBytes));
}
Validators.validateAesKeySize(keySizeInBytes);
if (ciphertextSegmentSize <= firstSegmentOffset + getHeaderLength() + TAG_SIZE_IN_BYTES) {
throw new InvalidAlgorithmParameterException("ciphertextSegmentSize too small");
}
this.ikm = Arrays.copyOf(ikm, ikm.length);
this.hkdfAlg = hkdfAlg;
this.keySizeInBytes = keySizeInBytes;
this.ciphertextSegmentSize = ciphertextSegmentSize;
this.firstSegmentOffset = firstSegmentOffset;
this.plaintextSegmentSize = ciphertextSegmentSize - TAG_SIZE_IN_BYTES;
}
@Override
public AesGcmHkdfStreamEncrypter newStreamSegmentEncrypter(byte[] aad)
throws GeneralSecurityException {
return new AesGcmHkdfStreamEncrypter(aad);
}
@Override
public AesGcmHkdfStreamDecrypter newStreamSegmentDecrypter() throws GeneralSecurityException {
return new AesGcmHkdfStreamDecrypter();
}
@Override
public int getPlaintextSegmentSize() {
return plaintextSegmentSize;
}
@Override
public int getCiphertextSegmentSize() {
return ciphertextSegmentSize;
}
@Override
public int getHeaderLength() {
return 1 + keySizeInBytes + NONCE_PREFIX_IN_BYTES;
}
@Override
public int getCiphertextOffset() {
return getHeaderLength() + firstSegmentOffset;
}
@Override
public int getCiphertextOverhead() {
return TAG_SIZE_IN_BYTES;
}
public int getFirstSegmentOffset() {
return firstSegmentOffset;
}
/**
* Returns the expected size of the ciphertext for a given plaintext. The returned value includes
* the header and offset.
*/
public long expectedCiphertextSize(long plaintextSize) {
long offset = getCiphertextOffset();
long fullSegments = (plaintextSize + offset) / plaintextSegmentSize;
long ciphertextSize = fullSegments * ciphertextSegmentSize;
long lastSegmentSize = (plaintextSize + offset) % plaintextSegmentSize;
if (lastSegmentSize > 0) {
ciphertextSize += lastSegmentSize + TAG_SIZE_IN_BYTES;
}
return ciphertextSize;
}
private static Cipher cipherInstance() throws GeneralSecurityException {
return EngineFactory.CIPHER.getInstance("AES/GCM/NoPadding");
}
private byte[] randomSalt() {
return Random.randBytes(keySizeInBytes);
}
private GCMParameterSpec paramsForSegment(byte[] prefix, int segmentNr, boolean last) {
ByteBuffer nonce = ByteBuffer.allocate(NONCE_SIZE_IN_BYTES);
nonce.order(ByteOrder.BIG_ENDIAN);
nonce.put(prefix);
nonce.putInt(segmentNr);
nonce.put((byte) (last ? 1 : 0));
return new GCMParameterSpec(8 * TAG_SIZE_IN_BYTES, nonce.array());
}
private byte[] randomNonce() {
return Random.randBytes(NONCE_PREFIX_IN_BYTES);
}
private SecretKeySpec deriveKeySpec(byte[] salt, byte[] aad) throws GeneralSecurityException {
byte[] key = Hkdf.computeHkdf(hkdfAlg, ikm, salt, aad, keySizeInBytes);
return new SecretKeySpec(key, "AES");
}
/**
* An instance of a crypter used to encrypt a plaintext stream. The instances have state:
* encryptedSegments counts the number of encrypted segments. This state is used to generate the
* IV for each segment. By enforcing that only the method encryptSegment can increment this state,
* we can guarantee that the IV does not repeat.
*/
class AesGcmHkdfStreamEncrypter implements StreamSegmentEncrypter {
private final SecretKeySpec keySpec;
private final Cipher cipher;
private final byte[] noncePrefix;
private ByteBuffer header;
private int encryptedSegments = 0;
public AesGcmHkdfStreamEncrypter(byte[] aad) throws GeneralSecurityException {
cipher = cipherInstance();
encryptedSegments = 0;
byte[] salt = randomSalt();
noncePrefix = randomNonce();
header = ByteBuffer.allocate(getHeaderLength());
header.put((byte) getHeaderLength());
header.put(salt);
header.put(noncePrefix);
header.flip();
keySpec = deriveKeySpec(salt, aad);
}
@Override
public ByteBuffer getHeader() {
return header.asReadOnlyBuffer();
}
/**
* Encrypts the next plaintext segment. This uses encryptedSegments as the segment number for
* the encryption.
*/
@Override
public synchronized void encryptSegment(
ByteBuffer plaintext, boolean isLastSegment, ByteBuffer ciphertext)
throws GeneralSecurityException {
cipher.init(
Cipher.ENCRYPT_MODE,
keySpec,
paramsForSegment(noncePrefix, encryptedSegments, isLastSegment));
encryptedSegments++;
cipher.doFinal(plaintext, ciphertext);
}
/**
* Encrypt a segment consisting of two parts. This method simplifies the case where one part of
* the plaintext is buffered and the other part is passed in by the caller.
*/
@Override
public synchronized void encryptSegment(
ByteBuffer part1, ByteBuffer part2, boolean isLastSegment, ByteBuffer ciphertext)
throws GeneralSecurityException {
cipher.init(
Cipher.ENCRYPT_MODE,
keySpec,
paramsForSegment(noncePrefix, encryptedSegments, isLastSegment));
encryptedSegments++;
// `update(nonEmpty)`, `doFinal(empty)` is known to cause problems on Android 23.
// See https://github.com/google/tink/issues/229
if (part2.hasRemaining()) {
cipher.update(part1, ciphertext);
cipher.doFinal(part2, ciphertext);
} else {
cipher.doFinal(part1, ciphertext);
}
}
}
/** An instance of a crypter used to decrypt a ciphertext stream. */
class AesGcmHkdfStreamDecrypter implements StreamSegmentDecrypter {
private SecretKeySpec keySpec;
private Cipher cipher;
private byte[] noncePrefix;
AesGcmHkdfStreamDecrypter() {};
@Override
public synchronized void init(ByteBuffer header, byte[] aad) throws GeneralSecurityException {
if (header.remaining() != getHeaderLength()) {
throw new InvalidAlgorithmParameterException("Invalid header length");
}
byte firstByte = header.get();
if (firstByte != getHeaderLength()) {
// We expect the first byte to be the length of the header.
// If this is not the case then either the ciphertext is incorrectly
// aligned or invalid.
throw new GeneralSecurityException("Invalid ciphertext");
}
noncePrefix = new byte[NONCE_PREFIX_IN_BYTES];
byte[] salt = new byte[keySizeInBytes];
header.get(salt);
header.get(noncePrefix);
keySpec = deriveKeySpec(salt, aad);
cipher = cipherInstance();
}
@Override
public synchronized void decryptSegment(
ByteBuffer ciphertext, int segmentNr, boolean isLastSegment, ByteBuffer plaintext)
throws GeneralSecurityException {
GCMParameterSpec params = paramsForSegment(noncePrefix, segmentNr, isLastSegment);
cipher.init(Cipher.DECRYPT_MODE, keySpec, params);
cipher.doFinal(ciphertext, plaintext);
}
}
}