diff options
author | Tor Brede Vekterli <vekterli@yahooinc.com> | 2023-01-04 17:22:54 +0100 |
---|---|---|
committer | Tor Brede Vekterli <vekterli@yahooinc.com> | 2023-01-05 15:23:38 +0100 |
commit | b9292918b2ec3c26492ae2424756080059a089b4 (patch) | |
tree | 18cb7dfd715759f0d64d0d67c574af3981e7cf21 /security-utils/src/test | |
parent | bb6638634f5bec608f62d710c97b0b97f79fc07f (diff) |
Use ChaCha20-Poly1305 instead of AES-GCM for shared key-based crypto
This is to get around the limitation where AES GCM can only produce
a maximum of 64 GiB of ciphertext for a particular <key, IV> pair before
its security properties break down. ChaCha20-Poly1305 does not have any
practical limitations here.
ChaCha20-Poly1305 uses a 256-bit key whereas the shared key is 128 bits.
A HKDF is used to internally expand the key material to 256 bits.
To let token based decryption be fully backwards compatible, introduce
a token version 2. V1 tokens will be decrypted with AES-GCM 128, while
V2 tokens use ChaCha20-Poly1305.
As a bonus, cryptographic operations will generally be _faster_ after
this cipher change, as we use BouncyCastle ciphers and these do not use
any native AES instructions. ChaCha20-Poly1305 is usually considerably
faster when running without specialized hardware support. An ad-hoc
experiment with a large ciphertext showed a near 70% performance increase
over AES-GCM 128.
Diffstat (limited to 'security-utils/src/test')
-rw-r--r-- | security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java | 68 |
1 files changed, 61 insertions, 7 deletions
diff --git a/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java b/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java index 25324ad7317..35b52d13b1d 100644 --- a/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java +++ b/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java @@ -59,20 +59,74 @@ public class SharedKeyTest { } @Test - void token_v1_representation_is_stable() { + void resealed_token_preserves_token_version_of_source_token() { + var originalPrivate = KeyUtils.fromBase58EncodedX25519PrivateKey("GFg54SaGNCmcSGufZCx68SKLGuAFrASoDeMk3t5AjU6L"); + var v1Token = "OntP9gRVAjXeZIr4zkYqRJFcnA993v7ZEE7VbcNs1NcR3HdE7Mpwlwi3r3anF1kVa5fn7O1CyeHQpBWpdayUTKkrtyFepG6WJrZdE"; + + var originalSealed = SealedSharedKey.fromTokenString(v1Token); + var originalSecret = SharedKeyGenerator.fromSealedKey(originalSealed, originalPrivate); + + var secondaryReceiverKp = KeyUtils.generateX25519KeyPair(); + var resealedShared = SharedKeyGenerator.reseal(originalSecret, secondaryReceiverKp.getPublic(), KEY_ID_2); + + var theirSealed = SealedSharedKey.fromTokenString(resealedShared.sealedSharedKey().toTokenString()); + assertEquals(1, theirSealed.tokenVersion()); + } + + @Test + void token_v1_representation_is_stable() throws IOException { var receiverPrivate = KeyUtils.fromBase58EncodedX25519PrivateKey("GFg54SaGNCmcSGufZCx68SKLGuAFrASoDeMk3t5AjU6L"); var receiverPublic = KeyUtils.fromBase58EncodedX25519PublicKey( "5drrkakYLjYSBpr5Haknh13EiCYL36ndMzK4gTJo6pwh"); var keyId = KeyId.ofString("my key ID"); - // Token generated for the above receiver public key, with the below expected shared secret (in hex) + // V1 token generated for the above receiver public key, with the below expected shared secret (in hex) var publicToken = "OntP9gRVAjXeZIr4zkYqRJFcnA993v7ZEE7VbcNs1NcR3HdE7Mpwlwi3r3anF1kVa5fn7O1CyeHQpBWpdayUTKkrtyFepG6WJrZdE"; var expectedSharedSecret = "1b33b4dcd6a94e5a4a1ee6d208197d01"; var theirSealed = SealedSharedKey.fromTokenString(publicToken); var theirShared = SharedKeyGenerator.fromSealedKey(theirSealed, receiverPrivate); + assertEquals(1, theirSealed.tokenVersion()); + assertEquals(keyId, theirSealed.keyId()); + assertEquals(expectedSharedSecret, hex(theirShared.secretKey().getEncoded())); + + // Encryption with v1 tokens must use AES-GCM 128 + var plaintext = "it's Bocchi time"; + var expectedCiphertext = "a2ba842b2e0769a4a2948c4236d4ae921f1dd05c2e094dcde9699eeefcc3d7ae"; + byte[] ct = streamEncryptString(plaintext, theirShared); + assertEquals(expectedCiphertext, hex(ct)); + + // Decryption with v1 tokens must use AES-GCM 128 + var decrypted = streamDecryptString(ct, theirShared); + assertEquals(plaintext, decrypted); + } + + @Test + void token_v2_representation_is_stable() throws IOException { + var receiverPrivate = KeyUtils.fromBase58EncodedX25519PrivateKey("GFg54SaGNCmcSGufZCx68SKLGuAFrASoDeMk3t5AjU6L"); + var receiverPublic = KeyUtils.fromBase58EncodedX25519PublicKey( "5drrkakYLjYSBpr5Haknh13EiCYL36ndMzK4gTJo6pwh"); + var keyId = KeyId.ofString("my key ID"); + + // V2 token generated for the above receiver public key, with the below expected shared secret (in hex) + var publicToken = "mjA83HYuulZW5SWV8FKz4m3b3m9zU8mTrX9n6iY4wZaA6ZNr8WnBZwOU4KQqhPCORPlzSYk4svlonzPZIb3Bjbqr2ePYKLOpdGhCO"; + var expectedSharedSecret = "205af82154690fd7b6d56a977563822c"; + + var theirSealed = SealedSharedKey.fromTokenString(publicToken); + var theirShared = SharedKeyGenerator.fromSealedKey(theirSealed, receiverPrivate); + + assertEquals(2, theirSealed.tokenVersion()); assertEquals(keyId, theirSealed.keyId()); assertEquals(expectedSharedSecret, hex(theirShared.secretKey().getEncoded())); + + // Encryption with v2 tokens must use ChaCha20-Poly1305 + var plaintext = "it's Bocchi time"; + var expectedCiphertext = "ea19dd0ac3ea6d76dc4e96430b0d5902a21cb3a27fa99490f4dcc391eaf5cec4"; + byte[] ct = streamEncryptString(plaintext, theirShared); + assertEquals(expectedCiphertext, hex(ct)); + + // Decryption with v2 tokens must use ChaCha20-Poly1305 + var decrypted = streamDecryptString(ct, theirShared); + assertEquals(plaintext, decrypted); } @Test @@ -102,7 +156,7 @@ public class SharedKeyTest { var mySealed = myShared.sealedSharedKey(); var badId = KeyId.ofString("my key 2"); - var tamperedShared = new SealedSharedKey(badId, mySealed.enc(), mySealed.ciphertext()); + var tamperedShared = new SealedSharedKey(SealedSharedKey.CURRENT_TOKEN_VERSION, badId, mySealed.enc(), mySealed.ciphertext()); // Should not be able to unseal the token since the AAD auth tag won't be correct assertThrows(RuntimeException.class, // TODO consider distinct exception class () -> SharedKeyGenerator.fromSealedKey(tamperedShared, keyPair.getPrivate())); @@ -130,7 +184,7 @@ public class SharedKeyTest { var myShared = SharedKeyGenerator.generateForReceiverPublicKey(keyPair.getPublic(), goodId); // token header is u8 version || u8 key id length || key id bytes ... - // Since the key ID is only 1 bytes long, patch it with a bad UTF-8 value + // Since the key ID is only 1 byte long, patch it with a bad UTF-8 value byte[] tokenBytes = Base62.codec().decode(myShared.sealedSharedKey().toTokenString()); tokenBytes[2] = (byte)0xC0; // First part of a 2-byte continuation without trailing byte var patchedTokenStr = Base62.codec().encode(tokenBytes); @@ -138,7 +192,7 @@ public class SharedKeyTest { } static byte[] streamEncryptString(String data, SecretSharedKey secretSharedKey) throws IOException { - var cipher = SharedKeyGenerator.makeAesGcmEncryptionCipher(secretSharedKey); + var cipher = secretSharedKey.makeEncryptionCipher(); var outStream = new ByteArrayOutputStream(); try (var cipherStream = cipher.wrapOutputStream(outStream)) { cipherStream.write(data.getBytes(StandardCharsets.UTF_8)); @@ -148,7 +202,7 @@ public class SharedKeyTest { } static String streamDecryptString(byte[] encrypted, SecretSharedKey secretSharedKey) throws IOException { - var cipher = SharedKeyGenerator.makeAesGcmDecryptionCipher(secretSharedKey); + var cipher = secretSharedKey.makeDecryptionCipher(); var inStream = new ByteArrayInputStream(encrypted); var total = ByteBuffer.allocate(encrypted.length); // Assume decrypted form can't be _longer_ byte[] tmp = new byte[8]; // short buf to test chunking @@ -198,7 +252,7 @@ public class SharedKeyTest { } private static void doOutputStreamCipherDecrypt(SecretSharedKey myShared, byte[] encrypted) throws Exception { - var cipher = SharedKeyGenerator.makeAesGcmDecryptionCipher(myShared); + var cipher = myShared.makeDecryptionCipher(); var outStream = new ByteArrayOutputStream(); try (var cipherStream = cipher.wrapOutputStream(outStream)) { cipherStream.write(encrypted); |