diff options
author | Tor Brede Vekterli <vekterli@yahooinc.com> | 2022-10-03 11:10:09 +0200 |
---|---|---|
committer | Tor Brede Vekterli <vekterli@yahooinc.com> | 2022-10-11 13:18:52 +0200 |
commit | 06f5f911249177f94893a5985e0f9db1fcf8b098 (patch) | |
tree | 72e7fd360b12caacefd40c77f6907590a134f104 /security-utils/src/test | |
parent | 2b67b2eedee700de6f19d377382361107918cd4d (diff) |
Add utilities for secure one-way single-use key exchange tokens using ECIES
Lets a sender generate a random, single-use symmetric key and securely
share this with a receiver, with the sender only knowing the public
key of the receiver. The shared key is exchanged via an opaque token
that can only be decoded by having the private key corresponding to
the public key used for encoding it.
This is implemented using ECIES, a hybrid encryption scheme using
Elliptic Curve Diffie-Hellman (ECDH) for ephemeral key exchange combined
with a symmetric cipher using the ephemeral key for actual plaintext
encryption/decryption.
In addition to the key exchange itself, utilities for creating
encryption and decryption ciphers for AES-GCM-256 from the shared keys
are included.
**Security note**: since the key is intended to be used for producing a
single piece of ciphertext, a fixed Initialization Vector (IV) is used.
The key MUST NOT be used to produce more than one ciphertext, lest the
entire security model breaks down entirely.
Diffstat (limited to 'security-utils/src/test')
-rw-r--r-- | security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java | 135 |
1 files changed, 135 insertions, 0 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 new file mode 100644 index 00000000000..c5f539dce58 --- /dev/null +++ b/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java @@ -0,0 +1,135 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; + +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class SharedKeyTest { + + @Test + void generated_secret_key_is_256_bit_aes() { + var receiverKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); + var shared = SharedKeyGenerator.generateForReceiverPublicKey(receiverKeyPair.getPublic(), 1); + var secret = shared.secretKey(); + assertEquals(secret.getAlgorithm(), "AES"); + assertEquals(secret.getEncoded().length, 32); + } + + @Test + void sealed_shared_key_can_be_exchanged_via_token_and_computes_identical_secret_key_at_receiver() { + var receiverKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); + + var myShared = SharedKeyGenerator.generateForReceiverPublicKey(receiverKeyPair.getPublic(), 1); + var publicToken = myShared.sealedSharedKey().toTokenString(); + + var theirSealed = SealedSharedKey.fromTokenString(publicToken); + var theirShared = SharedKeyGenerator.fromSealedKey(theirSealed, receiverKeyPair); + + assertArrayEquals(myShared.secretKey().getEncoded(), theirShared.secretKey().getEncoded()); + } + + @Test + void token_v1_representation_is_stable() { + var receiverPrivate = KeyUtils.fromPemEncodedPrivateKey( + """ + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEIO+CkAccoU9jPjX64mwU54Ar9DNZSLBBTYRSINerSW8EoAoGCCqGSM49 + AwEHoUQDQgAE3FA2VSuOn0vVhtQgNe13H2UE0Vx5A41demyX8nkHTCO4BDXSEPca + vejY7YaVcNSvFUbzDvia51X4pxbr1pe56g== + -----END EC PRIVATE KEY----- + """); + var receiverPublic = KeyUtils.fromPemEncodedPublicKey( + """ + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3FA2VSuOn0vVhtQgNe13H2UE0Vx5 + A41demyX8nkHTCO4BDXSEPcavejY7YaVcNSvFUbzDvia51X4pxbr1pe56g== + -----END PUBLIC KEY----- + """ + ); + var receiverKeyPair = new KeyPair(receiverPublic, receiverPrivate); + + // Token generated for the above receiver public key, with the below expected shared secret (in hex) + var publicToken = "AQAAAQTAVVthmrVAbMgKt8hBc4xColmDmEeEAyPD-ZcPlRmeId9wBaZTTctwV3pwT3FyV0UtvX_7zrRId" + + "3mNxvaru0tvFucd7mYY73Hi9d3j8qS6pN0bTTb1sw_dKYrR_0BXhEFE_py8uZnNxvV8-wHtBIAVXBk_4Q"; + var expectedSharedSecret = "b1aded9dc19593baa08fe64f916dcbaf3328ec666d2e0c81b1f6f8af9794187b"; + + var theirSealed = SealedSharedKey.fromTokenString(publicToken); + var theirShared = SharedKeyGenerator.fromSealedKey(theirSealed, receiverKeyPair); + + assertEquals(expectedSharedSecret, Hex.toHexString(theirShared.secretKey().getEncoded())); + } + + @Test + void unrelated_private_key_cannot_decrypt_shared_secret_key() { + var aliceKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); + var eveKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); + var bobShared = SharedKeyGenerator.generateForReceiverPublicKey(aliceKeyPair.getPublic(), 1); + assertThrows(IllegalArgumentException.class, // TODO consider distinct exception class + () -> SharedKeyGenerator.fromSealedKey(bobShared.sealedSharedKey(), eveKeyPair)); + } + + @Test + void token_carries_key_id_as_metadata() { + int keyId = 12345; + var keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); + var myShared = SharedKeyGenerator.generateForReceiverPublicKey(keyPair.getPublic(), keyId); + var publicToken = myShared.sealedSharedKey().toTokenString(); + var theirShared = SealedSharedKey.fromTokenString(publicToken); + assertEquals(theirShared.keyId(), keyId); + } + + static byte[] streamEncryptString(String data, SecretSharedKey secretSharedKey) throws IOException { + var cipher = SharedKeyGenerator.makeAes256GcmEncryptionCipher(secretSharedKey); + var outStream = new ByteArrayOutputStream(); + try (var cipherStream = new CipherOutputStream(outStream, cipher)) { + cipherStream.write(data.getBytes(StandardCharsets.UTF_8)); + cipherStream.flush(); + } + return outStream.toByteArray(); + } + + static String streamDecryptString(byte[] encrypted, SecretSharedKey secretSharedKey) throws IOException { + var cipher = SharedKeyGenerator.makeAes256GcmDecryptionCipher(secretSharedKey); + 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 + try (var cipherStream = new CipherInputStream(inStream, cipher)) { + while (true) { + int read = cipherStream.read(tmp); + if (read == -1) { + break; + } + total.put(tmp, 0, read); + } + } + total.flip(); + byte[] strBytes = new byte[total.remaining()]; + total.get(strBytes); + return new String(strBytes, StandardCharsets.UTF_8); + } + + @Test + void can_create_symmetric_ciphers_from_shared_secret_key_and_public_keys() throws Exception { + var receiverKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); + var myShared = SharedKeyGenerator.generateForReceiverPublicKey(receiverKeyPair.getPublic(), 1); + + String terrifyingSecret = "birds are not real D:"; + byte[] encrypted = streamEncryptString(terrifyingSecret, myShared); + String decrypted = streamDecryptString(encrypted, myShared); + assertEquals(terrifyingSecret, decrypted); + } + +} |