diff options
Diffstat (limited to 'security-utils/src/main/java')
-rw-r--r-- | security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java | 30 | ||||
-rw-r--r-- | security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java | 77 |
2 files changed, 51 insertions, 56 deletions
diff --git a/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java b/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java index 237c4976c7c..9c379b67a23 100644 --- a/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java +++ b/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java @@ -15,12 +15,12 @@ import java.util.Base64; * This token representation is expected to be used as a convenient serialization * form when communicating shared keys. */ -public record SealedSharedKey(int keyId, byte[] eciesPayload, byte[] iv) { +public record SealedSharedKey(int keyId, byte[] enc, byte[] ciphertext) { /** Current encoding version of opaque sealed key tokens. Must be less than 256. */ public static final int CURRENT_TOKEN_VERSION = 1; - private static final int ECIES_AES_IV_LENGTH = SharedKeyGenerator.ECIES_AES_CBC_IV_BITS / 8; + private static final int MAX_ENC_CONTEXT_LENGTH = Short.MAX_VALUE; /** * Creates an opaque URL-safe string token that contains enough information to losslessly @@ -30,14 +30,16 @@ public record SealedSharedKey(int keyId, byte[] eciesPayload, byte[] iv) { if (keyId >= (1 << 24)) { throw new IllegalArgumentException("Key id is too large to be encoded"); } - if (iv.length != ECIES_AES_IV_LENGTH) { - throw new IllegalStateException("Expected a %d byte IV, got %d bytes".formatted(ECIES_AES_IV_LENGTH, iv.length)); + if (enc.length > MAX_ENC_CONTEXT_LENGTH) { + throw new IllegalArgumentException("Encryption context is too large to be encoded"); } - ByteBuffer encoded = ByteBuffer.allocate(4 + ECIES_AES_IV_LENGTH + eciesPayload.length); + // i32 header || i16 length(enc) || enc || ciphertext + ByteBuffer encoded = ByteBuffer.allocate(4 + 2 + enc.length + ciphertext.length); encoded.putInt((CURRENT_TOKEN_VERSION << 24) | keyId); - encoded.put(iv); - encoded.put(eciesPayload); + encoded.putShort((short)enc.length); + encoded.put(enc); + encoded.put(ciphertext); encoded.flip(); byte[] encBytes = new byte[encoded.remaining()]; @@ -61,13 +63,17 @@ public record SealedSharedKey(int keyId, byte[] eciesPayload, byte[] iv) { throw new IllegalArgumentException("Token had unexpected version. Expected %d, was %d" .formatted(CURRENT_TOKEN_VERSION, version)); } - byte[] iv = new byte[ECIES_AES_IV_LENGTH]; - decoded.get(iv); - byte[] eciesPayload = new byte[decoded.remaining()]; - decoded.get(eciesPayload); + short encLen = decoded.getShort(); + if (encLen <= 0) { + throw new IllegalArgumentException("Token encryption context does not have a valid length"); + } + byte[] enc = new byte[encLen]; + decoded.get(enc); + byte[] ciphertext = new byte[decoded.remaining()]; + decoded.get(ciphertext); int keyId = versionAndKeyId & 0xffffff; - return new SealedSharedKey(keyId, eciesPayload, iv); + return new SealedSharedKey(keyId, enc, ciphertext); } } diff --git a/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java b/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java index 07e8243ec09..f9a16a78013 100644 --- a/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java +++ b/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java @@ -1,14 +1,16 @@ // 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.jcajce.provider.util.BadBlockException; -import org.bouncycastle.jce.spec.IESParameterSpec; +import com.yahoo.security.hpke.Aead; +import com.yahoo.security.hpke.Ciphersuite; +import com.yahoo.security.hpke.Hpke; +import com.yahoo.security.hpke.Kdf; +import com.yahoo.security.hpke.Kem; -import javax.crypto.BadPaddingException; import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.InvalidAlgorithmParameterException; @@ -17,10 +19,12 @@ import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; +import java.security.interfaces.XECPrivateKey; +import java.security.interfaces.XECPublicKey; /** * Implements both the sender and receiver sides of a secure, anonymous one-way - * key generation and exchange protocol implemented using ECIES; a hybrid crypto + * key generation and exchange protocol implemented using HPKE; a hybrid crypto * scheme built around elliptic curves. * * A shared key, once generated, may have its sealed component sent over a public @@ -37,52 +41,38 @@ import java.security.SecureRandom; */ public class SharedKeyGenerator { - private static final int AES_GCM_KEY_BITS = 256; + private static final int AES_GCM_KEY_BITS = 128; private static final int AES_GCM_AUTH_TAG_BITS = 128; private static final String AES_GCM_ALGO_SPEC = "AES/GCM/NoPadding"; - private static final String ECIES_CIPHER_NAME = "ECIESwithSHA256andAES-CBC"; - protected static final int ECIES_AES_CBC_IV_BITS = 128; - private static final int ECIES_HMAC_BITS = 256; - private static final int ECIES_AES_KEY_BITS = 256; + private static final byte[] EMPTY_BYTES = new byte[0]; private static final SecureRandom SHARED_CSPRNG = new SecureRandom(); + // Since the HPKE ciphersuite is not provided in the token, we must be very explicit about what it always is + private static final Ciphersuite HPKE_CIPHERSUITE = Ciphersuite.of(Kem.dHKemX25519HkdfSha256(), Kdf.hkdfSha256(), Aead.aesGcm128()); + private static final Hpke HPKE = Hpke.of(HPKE_CIPHERSUITE); - public static SecretSharedKey generateForReceiverPublicKey(PublicKey receiverPublicKey, int keyId) { + private static SecretKey generateRandomSecretAesKey() { try { var keyGen = KeyGenerator.getInstance("AES"); keyGen.init(AES_GCM_KEY_BITS, SHARED_CSPRNG); - var secretKey = keyGen.generateKey(); - - var cipher = Cipher.getInstance(ECIES_CIPHER_NAME, BouncyCastleProviderHolder.getInstance()); - byte[] iv = new byte[ECIES_AES_CBC_IV_BITS / 8]; - SHARED_CSPRNG.nextBytes(iv); - var iesParamSpec = new IESParameterSpec(null, null, ECIES_HMAC_BITS, ECIES_AES_KEY_BITS, iv); - - cipher.init(Cipher.ENCRYPT_MODE, receiverPublicKey, iesParamSpec); - byte[] eciesPayload = cipher.doFinal(secretKey.getEncoded()); - - var sealedSharedKey = new SealedSharedKey(keyId, eciesPayload, iv); - return new SecretSharedKey(secretKey, sealedSharedKey); - } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException - | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) { + return keyGen.generateKey(); + } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } - public static SecretSharedKey fromSealedKey(SealedSharedKey sealedKey, PrivateKey receiverPrivateKey) { - try { - var cipher = Cipher.getInstance(ECIES_CIPHER_NAME, BouncyCastleProviderHolder.getInstance()); - var iesParamSpec = new IESParameterSpec(null, null, ECIES_HMAC_BITS, ECIES_AES_KEY_BITS, sealedKey.iv()); - cipher.init(Cipher.DECRYPT_MODE, receiverPrivateKey, iesParamSpec); - byte[] secretKey = cipher.doFinal(sealedKey.eciesPayload()); + public static SecretSharedKey generateForReceiverPublicKey(PublicKey receiverPublicKey, int keyId) { + var secretKey = generateRandomSecretAesKey(); + // TODO do we want to tie the key ID to the sealing via AAD? + var sealed = HPKE.sealBase((XECPublicKey) receiverPublicKey, EMPTY_BYTES, EMPTY_BYTES, secretKey.getEncoded()); + var sealedSharedKey = new SealedSharedKey(keyId, sealed.enc(), sealed.ciphertext()); + return new SecretSharedKey(secretKey, sealedSharedKey); + } - return new SecretSharedKey(new SecretKeySpec(secretKey, "AES"), sealedKey); - } catch (BadBlockException e) { - throw new IllegalArgumentException("Token integrity check failed; token is either corrupt or was " + - "generated for a different public key"); - } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException - | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) { - throw new RuntimeException(e); - } + public static SecretSharedKey fromSealedKey(SealedSharedKey sealedKey, PrivateKey receiverPrivateKey) { + // TODO do we want to tie the key ID to the opening via AAD? + byte[] secretKeyBytes = HPKE.openBase(sealedKey.enc(), (XECPrivateKey) receiverPrivateKey, + EMPTY_BYTES, EMPTY_BYTES, sealedKey.ciphertext()); + return new SecretSharedKey(new SecretKeySpec(secretKeyBytes, "AES"), sealedKey); } // A given key+IV pair can only be used for one single encryption session, ever. @@ -92,15 +82,14 @@ public class SharedKeyGenerator { // token recipient (which would be the case if the IV were deterministically derived // from the recipient key and ephemeral ECDH public key), as that would preclude // support for delegated key forwarding. - private static byte[] fixed96BitIvForSingleUseKey() { - // Nothing up my sleeve! - return new byte[] { 'h', 'e', 'r', 'e', 'B', 'd', 'r', 'a', 'g', 'o', 'n', 's' }; - } + private static final byte[] FIXED_96BIT_IV_FOR_SINGLE_USE_KEY = new byte[] { + 'h','e','r','e','B','d','r','a','g','o','n','s' // Nothing up my sleeve! + }; private static Cipher makeAesGcmCipher(SecretSharedKey secretSharedKey, int cipherMode) { try { var cipher = Cipher.getInstance(AES_GCM_ALGO_SPEC); - var gcmSpec = new GCMParameterSpec(AES_GCM_AUTH_TAG_BITS, fixed96BitIvForSingleUseKey()); + var gcmSpec = new GCMParameterSpec(AES_GCM_AUTH_TAG_BITS, FIXED_96BIT_IV_FOR_SINGLE_USE_KEY); cipher.init(cipherMode, secretSharedKey.secretKey(), gcmSpec); return cipher; } catch (NoSuchAlgorithmException | NoSuchPaddingException |