aboutsummaryrefslogtreecommitdiffstats
path: root/security-utils/src/test
diff options
context:
space:
mode:
authorTor Brede Vekterli <vekterli@yahooinc.com>2022-10-03 11:10:09 +0200
committerTor Brede Vekterli <vekterli@yahooinc.com>2022-10-11 13:18:52 +0200
commit06f5f911249177f94893a5985e0f9db1fcf8b098 (patch)
tree72e7fd360b12caacefd40c77f6907590a134f104 /security-utils/src/test
parent2b67b2eedee700de6f19d377382361107918cd4d (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.java135
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);
+ }
+
+}