Skip to content

Commit 0b4f709

Browse files
juergwcopybara-github
authored andcommittedNov 27, 2023
Add Cipher validation to AES GCM SIV.
This resolved the issue: #18. #tinkApiChange Encryption and decryption with AES GCM SIV on Android API versions 29 and older will now fail, instead of using the wrong algorithm. For each thread this primitive is used with, a decryption will be run to validate the cipher. We don't expect this to be a big performance penalty, as creating and starting a thread will be more costly than this additional check. This check could also be done on initialization of the class, or on creation of the object. But I'm worried that this might cause problems in rare cases when initialization is done before AES GCM SIV is registered. So I prefer not to do this. This could be solved in a completely different way, by checking the state of the system (for example, which android version is used, or which provides are available). But I think such an approach would be more brittle than this solution here. PiperOrigin-RevId: 585546163 Change-Id: I457ea40b21cf702688c9b29e4a9e286428bf0a39
1 parent 3f4d69e commit 0b4f709

File tree

4 files changed

+135
-21
lines changed

4 files changed

+135
-21
lines changed
 

‎src/main/java/com/google/crypto/tink/aead/subtle/AesGcmSiv.java

+48-11
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.google.crypto.tink.annotations.Alpha;
2626
import com.google.crypto.tink.subtle.Bytes;
2727
import com.google.crypto.tink.subtle.EngineFactory;
28+
import com.google.crypto.tink.subtle.Hex;
2829
import com.google.crypto.tink.subtle.Random;
2930
import com.google.crypto.tink.subtle.SubtleUtil;
3031
import com.google.crypto.tink.subtle.Validators;
@@ -49,12 +50,41 @@
4950
*/
5051
@Alpha
5152
public final class AesGcmSiv implements Aead {
52-
private static final ThreadLocal<Cipher> localCipher =
53+
54+
// Test vector from https://www.rfc-editor.org/rfc/rfc8452.html#appendix-C.1
55+
private static final byte[] TEST_PLAINTEXT = Hex.decode("7a806c");
56+
private static final byte[] TEST_AAD = Hex.decode("46bb91c3c5");
57+
private static final byte[] TEST_KEY = Hex.decode("36864200e0eaf5284d884a0e77d31646");
58+
private static final byte[] TEST_NOUNCE = Hex.decode("bae8e37fc83441b16034566b");
59+
private static final byte[] TEST_RESULT = Hex.decode("af60eb711bd85bc1e4d3e0a462e074eea428a8");
60+
61+
// On Android API version 29 and older, the security provider returns an AES GCM cipher instead
62+
// an AES GCM SIV cipher. This function tests if we have a correct cipher.
63+
private static boolean isAesGcmSivCipher(Cipher cipher) {
64+
try {
65+
// Use test vector to validate that cipher implements AES GCM SIV.
66+
AlgorithmParameterSpec params = getParams(TEST_NOUNCE);
67+
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(TEST_KEY, "AES"), params);
68+
cipher.updateAAD(TEST_AAD);
69+
byte[] output = cipher.doFinal(TEST_RESULT, 0, TEST_RESULT.length);
70+
return Bytes.equal(output, TEST_PLAINTEXT);
71+
} catch (GeneralSecurityException ex) {
72+
return false;
73+
}
74+
}
75+
76+
// localAesGcmSivCipher.get() may be null if the cipher returned by EngineFactory is not a valid
77+
// AES GCM SIV cipher.
78+
private static final ThreadLocal<Cipher> localAesGcmSivCipher =
5379
new ThreadLocal<Cipher>() {
5480
@Override
5581
protected Cipher initialValue() {
5682
try {
57-
return EngineFactory.CIPHER.getInstance("AES/GCM-SIV/NoPadding");
83+
Cipher cipher = EngineFactory.CIPHER.getInstance("AES/GCM-SIV/NoPadding");
84+
if (!isAesGcmSivCipher(cipher)) {
85+
return null;
86+
}
87+
return cipher;
5888
} catch (GeneralSecurityException ex) {
5989
throw new IllegalStateException(ex);
6090
}
@@ -86,8 +116,17 @@ public AesGcmSiv(final byte[] key) throws GeneralSecurityException {
86116
this(key, new byte[0]);
87117
}
88118

119+
private Cipher getAesGcmSivCipher() throws GeneralSecurityException {
120+
Cipher cipher = localAesGcmSivCipher.get();
121+
if (cipher == null) {
122+
throw new GeneralSecurityException("AES GCM SIV cipher is not available or is invalid.");
123+
}
124+
return cipher;
125+
}
126+
89127
private byte[] rawEncrypt(final byte[] plaintext, final byte[] associatedData)
90128
throws GeneralSecurityException {
129+
Cipher cipher = getAesGcmSivCipher();
91130
// Check that ciphertext is not longer than the max. size of a Java array.
92131
if (plaintext.length > Integer.MAX_VALUE - IV_SIZE_IN_BYTES - TAG_SIZE_IN_BYTES) {
93132
throw new GeneralSecurityException("plaintext too long");
@@ -97,12 +136,11 @@ private byte[] rawEncrypt(final byte[] plaintext, final byte[] associatedData)
97136
System.arraycopy(iv, 0, ciphertext, 0, IV_SIZE_IN_BYTES);
98137

99138
AlgorithmParameterSpec params = getParams(iv);
100-
localCipher.get().init(Cipher.ENCRYPT_MODE, keySpec, params);
139+
cipher.init(Cipher.ENCRYPT_MODE, keySpec, params);
101140
if (associatedData != null && associatedData.length != 0) {
102-
localCipher.get().updateAAD(associatedData);
141+
cipher.updateAAD(associatedData);
103142
}
104-
int written =
105-
localCipher.get().doFinal(plaintext, 0, plaintext.length, ciphertext, IV_SIZE_IN_BYTES);
143+
int written = cipher.doFinal(plaintext, 0, plaintext.length, ciphertext, IV_SIZE_IN_BYTES);
106144
// For security reasons, AES-GCM encryption must always use tag of TAG_SIZE_IN_BYTES bytes. If
107145
// so, written must be equal to plaintext.length + TAG_SIZE_IN_BYTES.
108146

@@ -134,18 +172,17 @@ public byte[] encrypt(final byte[] plaintext, final byte[] associatedData)
134172

135173
private byte[] rawDecrypt(final byte[] ciphertext, final byte[] associatedData)
136174
throws GeneralSecurityException {
175+
Cipher cipher = getAesGcmSivCipher();
137176
if (ciphertext.length < IV_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES) {
138177
throw new GeneralSecurityException("ciphertext too short");
139178
}
140179

141180
AlgorithmParameterSpec params = getParams(ciphertext, 0, IV_SIZE_IN_BYTES);
142-
localCipher.get().init(Cipher.DECRYPT_MODE, keySpec, params);
181+
cipher.init(Cipher.DECRYPT_MODE, keySpec, params);
143182
if (associatedData != null && associatedData.length != 0) {
144-
localCipher.get().updateAAD(associatedData);
183+
cipher.updateAAD(associatedData);
145184
}
146-
return localCipher
147-
.get()
148-
.doFinal(ciphertext, IV_SIZE_IN_BYTES, ciphertext.length - IV_SIZE_IN_BYTES);
185+
return cipher.doFinal(ciphertext, IV_SIZE_IN_BYTES, ciphertext.length - IV_SIZE_IN_BYTES);
149186
}
150187

151188
/**

‎src/main/java/com/google/crypto/tink/aead/subtle/BUILD.bazel

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ java_library(
3333
"//src/main/java/com/google/crypto/tink/annotations:alpha",
3434
"//src/main/java/com/google/crypto/tink/internal:util",
3535
"//src/main/java/com/google/crypto/tink/subtle:bytes",
36+
"//src/main/java/com/google/crypto/tink/subtle:hex",
3637
"//src/main/java/com/google/crypto/tink/subtle:random",
3738
"//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
3839
"//src/main/java/com/google/crypto/tink/subtle:validators",
@@ -70,6 +71,7 @@ android_library(
7071
"//src/main/java/com/google/crypto/tink/annotations:alpha-android",
7172
"//src/main/java/com/google/crypto/tink/internal:util-android",
7273
"//src/main/java/com/google/crypto/tink/subtle:bytes-android",
74+
"//src/main/java/com/google/crypto/tink/subtle:hex-android",
7375
"//src/main/java/com/google/crypto/tink/subtle:random-android",
7476
"//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
7577
"//src/main/java/com/google/crypto/tink/subtle:validators-android",

‎src/test/java/com/google/crypto/tink/aead/AesGcmSivKeyManagerTest.java

+65-4
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ public void createKey_multipleTimes() throws Exception {
142142

143143
@Test
144144
public void getPrimitive() throws Exception {
145+
@Nullable Integer apiLevel = Util.getAndroidApiLevel();
146+
Assume.assumeTrue(apiLevel == null || apiLevel >= 30); // Run the test on java and android >= 30
147+
145148
AesGcmSivKey key = factory.createKey(AesGcmSivKeyFormat.newBuilder().setKeySize(16).build());
146149
Aead managerAead = manager.getPrimitive(key, Aead.class);
147150
Aead directAead = new AesGcmSiv(key.getKeyValue().toByteArray());
@@ -154,6 +157,9 @@ public void getPrimitive() throws Exception {
154157

155158
@Test
156159
public void testCiphertextSize() throws Exception {
160+
@Nullable Integer apiLevel = Util.getAndroidApiLevel();
161+
Assume.assumeTrue(apiLevel == null || apiLevel >= 30); // Run the test on java and android >= 30
162+
157163
AesGcmSivKey key = factory.createKey(AesGcmSivKeyFormat.newBuilder().setKeySize(32).build());
158164
Aead aead = new AesGcmSivKeyManager().getPrimitive(key, Aead.class);
159165
byte[] plaintext = "plaintext".getBytes(UTF_8);
@@ -290,6 +296,9 @@ public void testCreateKeyFromRandomness_slowInputStream_works() throws Exception
290296

291297
@Test
292298
public void testEncryptDecrypt_works() throws Exception {
299+
@Nullable Integer apiLevel = Util.getAndroidApiLevel();
300+
Assume.assumeTrue(apiLevel == null || apiLevel >= 30); // Run the test on java and android >= 30
301+
293302
com.google.crypto.tink.aead.AesGcmSivKey aesGcmSivKey =
294303
com.google.crypto.tink.aead.AesGcmSivKey.builder()
295304
.setParameters(
@@ -315,13 +324,11 @@ public void testEncryptDecrypt_works() throws Exception {
315324
}
316325

317326
@Test
318-
public void testGetPrimitiveReturnsAesGcmBeforeAndroid30() throws Exception {
327+
public void testEncryptAndDecryptFailBeforeAndroid30() throws Exception {
319328
@Nullable Integer apiLevel = Util.getAndroidApiLevel();
320329
Assume.assumeNotNull(apiLevel);
321330
Assume.assumeTrue(apiLevel < 30);
322331

323-
// TODO(b/303637541) This is a bug, it should instead throw an error.
324-
325332
// Use an AES GCM test vector from AesGcmJceTest.testWithAesGcmKey_noPrefix_works
326333
byte[] keyBytes = Hex.decode("5b9604fe14eadba931b0ccf34843dab9");
327334
AesGcmSivParameters parameters =
@@ -341,7 +348,61 @@ public void testGetPrimitiveReturnsAesGcmBeforeAndroid30() throws Exception {
341348
.build();
342349
Aead aead = keysetHandle.getPrimitive(Aead.class);
343350

351+
assertThrows(GeneralSecurityException.class, () -> aead.encrypt(new byte[] {}, new byte[] {}));
344352
byte[] fixedCiphertext = Hex.decode("c3561ce7f48b8a6b9b8d5ef957d2e512368f7da837bcf2aeebe176e3");
345-
assertThat(aead.decrypt(fixedCiphertext, new byte[] {})).isEmpty();
353+
assertThrows(
354+
GeneralSecurityException.class, () -> aead.decrypt(fixedCiphertext, new byte[] {}));
355+
}
356+
357+
// This test shows how ciphertexts created with older versions of Tink on older versions of
358+
// Android can still be decrypted with the current version of Tink.
359+
@Test
360+
public void testDecryptCiphertextCreatedOnOlderVersionOfAndroid() throws Exception {
361+
@Nullable Integer apiLevel = Util.getAndroidApiLevel();
362+
Assume.assumeTrue(apiLevel == null || apiLevel >= 30); // Run the test on java and android >= 30
363+
364+
// A valid AES GCM SIV key.
365+
com.google.crypto.tink.aead.AesGcmSivKey aesGcmSivKey =
366+
com.google.crypto.tink.aead.AesGcmSivKey.builder()
367+
.setParameters(
368+
AesGcmSivParameters.builder()
369+
.setKeySizeBytes(16)
370+
.setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
371+
.build())
372+
.setKeyBytes(
373+
SecretBytes.copyFrom(
374+
Hex.decode("5b9604fe14eadba931b0ccf34843dab9"), InsecureSecretKeyAccess.get()))
375+
.build();
376+
377+
// Valid ciphertext of an empty plaintext created with aesGcmSivKey.
378+
byte[] validCiphertext = Hex.decode("17871550708697c27881d04753337526f2bed57b7e2eac30ecde0202");
379+
380+
// Ciphertext created with aesGcmSivKey on Android version 29 before
381+
// https://github.com/tink-crypto/tink-java/issues/18 was fixed.
382+
byte[] legacyCiphertext =
383+
Hex.decode("c3561ce7f48b8a6b9b8d5ef957d2e512368f7da837bcf2aeebe176e3");
384+
385+
// Create an Aead instance that can decrypt in both AES GCM and AES GCM SIV.
386+
com.google.crypto.tink.aead.AesGcmKey legacyKey =
387+
com.google.crypto.tink.aead.AesGcmKey.builder()
388+
.setParameters(
389+
AesGcmParameters.builder()
390+
.setIvSizeBytes(12)
391+
.setKeySizeBytes(16)
392+
.setTagSizeBytes(16)
393+
.setVariant(AesGcmParameters.Variant.NO_PREFIX)
394+
.build())
395+
.setKeyBytes(aesGcmSivKey.getKeyBytes())
396+
.build();
397+
KeysetHandle backwardsCompatibleKeysetHandle =
398+
KeysetHandle.newBuilder()
399+
.addEntry(KeysetHandle.importKey(aesGcmSivKey).withRandomId().makePrimary())
400+
.addEntry(KeysetHandle.importKey(legacyKey).withRandomId())
401+
.build();
402+
Aead backwardsCompatibleAead = backwardsCompatibleKeysetHandle.getPrimitive(Aead.class);
403+
404+
// Check that backwardsCompatibleAead can decrypt both valid and legacy ciphertexts.
405+
assertThat(backwardsCompatibleAead.decrypt(validCiphertext, new byte[] {})).isEmpty();
406+
assertThat(backwardsCompatibleAead.decrypt(legacyCiphertext, new byte[] {})).isEmpty();
346407
}
347408
}

‎src/test/java/com/google/crypto/tink/aead/subtle/AesGcmSivTest.java

+20-6
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ public void setUpConscrypt() throws Exception {
7474

7575
@Test
7676
public void testEncryptDecrypt() throws Exception {
77+
@Nullable Integer apiLevel = Util.getAndroidApiLevel();
78+
Assume.assumeTrue(apiLevel == null || apiLevel >= 30); // Run the test on java and android >= 30
79+
7780
byte[] aad = new byte[] {1, 2, 3};
7881
for (int keySize : keySizeInBytes) {
7982
byte[] key = Random.randBytes(keySize);
@@ -111,6 +114,9 @@ public void testLongMessages() throws Exception {
111114

112115
@Test
113116
public void testModifyCiphertext() throws Exception {
117+
@Nullable Integer apiLevel = Util.getAndroidApiLevel();
118+
Assume.assumeTrue(apiLevel == null || apiLevel >= 30); // Run the test on java and android >= 30
119+
114120
byte[] aad = Random.randBytes(33);
115121
byte[] key = Random.randBytes(16);
116122
byte[] message = Random.randBytes(32);
@@ -215,6 +221,9 @@ public void testWycheproofVectors() throws Exception {
215221

216222
@Test
217223
public void testNullPlaintextOrCiphertext() throws Exception {
224+
@Nullable Integer apiLevel = Util.getAndroidApiLevel();
225+
Assume.assumeTrue(apiLevel == null || apiLevel >= 30); // Run the test on java and android >= 30
226+
218227
for (int keySize : keySizeInBytes) {
219228
AesGcmSiv gcm = new AesGcmSiv(Random.randBytes(keySize));
220229
byte[] aad = new byte[] {1, 2, 3};
@@ -243,6 +252,9 @@ public void testNullPlaintextOrCiphertext() throws Exception {
243252

244253
@Test
245254
public void testEmptyAssociatedData() throws Exception {
255+
@Nullable Integer apiLevel = Util.getAndroidApiLevel();
256+
Assume.assumeTrue(apiLevel == null || apiLevel >= 30); // Run the test on java and android >= 30
257+
246258
byte[] aad = new byte[0];
247259
for (int keySize : keySizeInBytes) {
248260
byte[] key = Random.randBytes(keySize);
@@ -285,6 +297,9 @@ public void testEmptyAssociatedData() throws Exception {
285297
* multiple ciphertexts of the same message are distinct.
286298
*/
287299
public void testRandomNonce() throws Exception {
300+
@Nullable Integer apiLevel = Util.getAndroidApiLevel();
301+
Assume.assumeTrue(apiLevel == null || apiLevel >= 30); // Run the test on java and android >= 30
302+
288303
final int samples = 1 << 17;
289304
byte[] key = Random.randBytes(16);
290305
byte[] message = new byte[0];
@@ -300,13 +315,11 @@ public void testRandomNonce() throws Exception {
300315
}
301316

302317
@Test
303-
public void testCreateImplementsAesGcmBeforeAndroid30() throws Exception {
318+
public void testCreate_encryptAndDecryptFailBeforeAndroid30() throws Exception {
304319
@Nullable Integer apiLevel = Util.getAndroidApiLevel();
305320
Assume.assumeNotNull(apiLevel);
306321
Assume.assumeTrue(apiLevel < 30);
307322

308-
// TODO(b/303637541) This is a bug, it should instead throw an error.
309-
310323
// Use an AES GCM test vector from AesGcmJceTest.testWithAesGcmKey_noPrefix_works
311324
byte[] keyBytes = Hex.decode("5b9604fe14eadba931b0ccf34843dab9");
312325
AesGcmSivParameters parameters =
@@ -321,8 +334,10 @@ public void testCreateImplementsAesGcmBeforeAndroid30() throws Exception {
321334
.build();
322335
Aead aead = AesGcmSiv.create(key);
323336

337+
assertThrows(GeneralSecurityException.class, () -> aead.encrypt(new byte[] {}, new byte[] {}));
324338
byte[] fixedCiphertext = Hex.decode("c3561ce7f48b8a6b9b8d5ef957d2e512368f7da837bcf2aeebe176e3");
325-
assertThat(aead.decrypt(fixedCiphertext, new byte[] {})).isEmpty();
339+
assertThrows(
340+
GeneralSecurityException.class, () -> aead.decrypt(fixedCiphertext, new byte[] {}));
326341
}
327342

328343
@Test
@@ -395,8 +410,7 @@ public void testWithAesGcmSivKey_tinkPrefix_works() throws Exception {
395410
@Test
396411
public void testWithAesGcmSivKey_crunchyPrefix_works() throws Exception {
397412
@Nullable Integer apiLevel = Util.getAndroidApiLevel();
398-
Assume.assumeNotNull(apiLevel);
399-
Assume.assumeTrue(apiLevel >= 30);
413+
Assume.assumeTrue(apiLevel == null || apiLevel >= 30); // Run the test on java and android >= 30
400414

401415
// Test vector draft-irtf-cfrg-gcmsiv-09 in Wycheproof
402416
byte[] plaintext = Hex.decode("7a806c");

0 commit comments

Comments
 (0)
Please sign in to comment.