blob: b14d30893d818e9746855531c90570badf8b1aa0 [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.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Streaming encryption using AES-CTR and HMAC.
*
* <p>Each ciphertext uses a new AES-CTR key and HMAC key that ared derived from the key derivation
* key, a randomly chosen salt of the same size as the key and a nonce prefix using HKDF.
*
* <p>The 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.
*
* @since 1.1.0
*/
public final class AesCtrHmacStreaming 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.
//
// The size of the nonce for AES-CTR
private static final int NONCE_SIZE_IN_BYTES = 16;
// The nonce has the format nonce_prefix || ctr || last_block || 0 0 0 0
// 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;
private static final int HMAC_KEY_SIZE_IN_BYTES = 32;
private final int keySizeInBytes;
private final String tagAlgo;
private final int tagSizeInBytes;
private final int ciphertextSegmentSize;
private final int plaintextSegmentSize;
private final int firstSegmentOffset;
private final String hkdfAlgo;
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 tagAlgo the JCE MAC algorithm name, e.g., HmacSha256, used for authentication.
* @param tagSizeInBytes the size authentication tags
* @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 AesCtrHmacStreaming(
byte[] ikm,
String hkdfAlgo,
int keySizeInBytes,
String tagAlgo,
int tagSizeInBytes,
int ciphertextSegmentSize,
int firstSegmentOffset)
throws InvalidAlgorithmParameterException {
validateParameters(
ikm.length,
keySizeInBytes,
tagAlgo,
tagSizeInBytes,
ciphertextSegmentSize,
firstSegmentOffset);
this.ikm = Arrays.copyOf(ikm, ikm.length);
this.hkdfAlgo = hkdfAlgo;
this.keySizeInBytes = keySizeInBytes;
this.tagAlgo = tagAlgo;
this.tagSizeInBytes = tagSizeInBytes;
this.ciphertextSegmentSize = ciphertextSegmentSize;
this.firstSegmentOffset = firstSegmentOffset;
this.plaintextSegmentSize = ciphertextSegmentSize - tagSizeInBytes;
}
private static void validateParameters(
int ikmSize,
int keySizeInBytes,
String tagAlgo,
int tagSizeInBytes,
int ciphertextSegmentSize,
int firstSegmentOffset)
throws InvalidAlgorithmParameterException {
if (ikmSize < 16 || ikmSize < keySizeInBytes) {
throw new InvalidAlgorithmParameterException(
"ikm too short, must be >= " + Math.max(16, keySizeInBytes));
}
Validators.validateAesKeySize(keySizeInBytes);
if (tagSizeInBytes < 10) {
throw new InvalidAlgorithmParameterException("tag size too small " + tagSizeInBytes);
}
if ((tagAlgo.equals("HmacSha1") && tagSizeInBytes > 20)
|| (tagAlgo.equals("HmacSha256") && tagSizeInBytes > 32)
|| (tagAlgo.equals("HmacSha512") && tagSizeInBytes > 64)) {
throw new InvalidAlgorithmParameterException("tag size too big");
}
int firstPlaintextSegment =
ciphertextSegmentSize
- firstSegmentOffset
- tagSizeInBytes
- keySizeInBytes
- NONCE_PREFIX_IN_BYTES
- 1;
if (firstPlaintextSegment <= 0) {
throw new InvalidAlgorithmParameterException("ciphertextSegmentSize too small");
}
}
@Override
public AesCtrHmacStreamEncrypter newStreamSegmentEncrypter(byte[] aad)
throws GeneralSecurityException {
return new AesCtrHmacStreamEncrypter(aad);
}
@Override
public AesCtrHmacStreamDecrypter newStreamSegmentDecrypter() throws GeneralSecurityException {
return new AesCtrHmacStreamDecrypter();
}
@Override
public int getCiphertextSegmentSize() {
return ciphertextSegmentSize;
}
@Override
public int getPlaintextSegmentSize() {
return plaintextSegmentSize;
}
@Override
public int getHeaderLength() {
return 1 + keySizeInBytes + NONCE_PREFIX_IN_BYTES;
}
@Override
public int getCiphertextOffset() {
return getHeaderLength() + firstSegmentOffset;
}
@Override
public int getCiphertextOverhead() {
return tagSizeInBytes;
}
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 + tagSizeInBytes;
}
return ciphertextSize;
}
private static Cipher cipherInstance() throws GeneralSecurityException {
return EngineFactory.CIPHER.getInstance("AES/CTR/NoPadding");
}
private Mac macInstance() throws GeneralSecurityException {
return EngineFactory.MAC.getInstance(tagAlgo);
}
private byte[] randomSalt() {
return Random.randBytes(keySizeInBytes);
}
private byte[] nonceForSegment(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));
nonce.putInt(0);
return nonce.array();
}
private byte[] randomNonce() {
return Random.randBytes(NONCE_PREFIX_IN_BYTES);
}
private byte[] deriveKeyMaterial(byte[] salt, byte[] aad) throws GeneralSecurityException {
int keyMaterialSize = keySizeInBytes + HMAC_KEY_SIZE_IN_BYTES;
return Hkdf.computeHkdf(hkdfAlgo, ikm, salt, aad, keyMaterialSize);
}
private SecretKeySpec deriveKeySpec(byte[] keyMaterial) throws GeneralSecurityException {
return new SecretKeySpec(keyMaterial, 0, keySizeInBytes, "AES");
}
private SecretKeySpec deriveHmacKeySpec(byte[] keyMaterial) throws GeneralSecurityException {
return new SecretKeySpec(keyMaterial, keySizeInBytes, HMAC_KEY_SIZE_IN_BYTES, tagAlgo);
}
/**
* 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 AesCtrHmacStreamEncrypter implements StreamSegmentEncrypter {
private final SecretKeySpec keySpec;
private final SecretKeySpec hmacKeySpec;
private final Cipher cipher;
private final Mac mac;
private final byte[] noncePrefix;
private ByteBuffer header;
private int encryptedSegments = 0;
public AesCtrHmacStreamEncrypter(byte[] aad) throws GeneralSecurityException {
cipher = cipherInstance();
mac = macInstance();
encryptedSegments = 0;
byte[] salt = randomSalt();
noncePrefix = randomNonce();
header = ByteBuffer.allocate(getHeaderLength());
header.put((byte) getHeaderLength());
header.put(salt);
header.put(noncePrefix);
header.flip();
byte[] keymaterial = deriveKeyMaterial(salt, aad);
keySpec = deriveKeySpec(keymaterial);
hmacKeySpec = deriveHmacKeySpec(keymaterial);
}
@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 {
int position = ciphertext.position();
byte[] nonce = nonceForSegment(noncePrefix, encryptedSegments, isLastSegment);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(nonce));
encryptedSegments++;
cipher.doFinal(plaintext, ciphertext);
ByteBuffer ctCopy = ciphertext.duplicate();
ctCopy.flip();
ctCopy.position(position);
mac.init(hmacKeySpec);
mac.update(nonce);
mac.update(ctCopy);
byte[] tag = mac.doFinal();
ciphertext.put(tag, 0, tagSizeInBytes);
}
/**
* 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 {
int position = ciphertext.position();
byte[] nonce = nonceForSegment(noncePrefix, encryptedSegments, isLastSegment);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(nonce));
encryptedSegments++;
cipher.update(part1, ciphertext);
cipher.doFinal(part2, ciphertext);
ByteBuffer ctCopy = ciphertext.duplicate();
ctCopy.flip();
ctCopy.position(position);
mac.init(hmacKeySpec);
mac.update(nonce);
mac.update(ctCopy);
byte[] tag = mac.doFinal();
ciphertext.put(tag, 0, tagSizeInBytes);
}
@Override
// TODO(b/74250492): So far this is unused.
public synchronized int getEncryptedSegments() {
return encryptedSegments;
}
}
/** An instance of a crypter used to decrypt a ciphertext stream. */
class AesCtrHmacStreamDecrypter implements StreamSegmentDecrypter {
private SecretKeySpec keySpec;
private SecretKeySpec hmacKeySpec;
private Cipher cipher;
private Mac mac;
private byte[] noncePrefix;
AesCtrHmacStreamDecrypter() {};
@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);
byte[] keymaterial = deriveKeyMaterial(salt, aad);
keySpec = deriveKeySpec(keymaterial);
hmacKeySpec = deriveHmacKeySpec(keymaterial);
cipher = cipherInstance();
mac = macInstance();
}
@Override
public synchronized void decryptSegment(
ByteBuffer ciphertext, int segmentNr, boolean isLastSegment, ByteBuffer plaintext)
throws GeneralSecurityException {
int position = ciphertext.position();
byte[] nonce = nonceForSegment(noncePrefix, segmentNr, isLastSegment);
int ctLength = ciphertext.remaining();
if (ctLength < tagSizeInBytes) {
throw new GeneralSecurityException("Ciphertext too short");
}
int ptLength = ctLength - tagSizeInBytes;
int startOfTag = position + ptLength;
ByteBuffer ct = ciphertext.duplicate();
ct.limit(startOfTag);
ByteBuffer tagBuffer = ciphertext.duplicate();
tagBuffer.position(startOfTag);
assert mac != null;
assert hmacKeySpec != null;
mac.init(hmacKeySpec);
mac.update(nonce);
mac.update(ct);
byte[] tag = mac.doFinal();
tag = Arrays.copyOf(tag, tagSizeInBytes);
byte[] expectedTag = new byte[tagSizeInBytes];
assert tagBuffer.remaining() == tagSizeInBytes;
tagBuffer.get(expectedTag);
assert expectedTag.length == tag.length;
if (!Bytes.equal(expectedTag, tag)) {
throw new GeneralSecurityException("Tag mismatch");
}
ciphertext.limit(startOfTag);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(nonce));
cipher.doFinal(ciphertext, plaintext);
}
}
}