diff options
Diffstat (limited to 'security-utils/src/main/java/com/yahoo')
3 files changed, 100 insertions, 25 deletions
diff --git a/security-utils/src/main/java/com/yahoo/security/KeyId.java b/security-utils/src/main/java/com/yahoo/security/KeyId.java new file mode 100644 index 00000000000..08e137eff03 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/KeyId.java @@ -0,0 +1,89 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security; + +import java.util.Arrays; +import java.util.Objects; + +import static com.yahoo.security.ArrayUtils.fromUtf8Bytes; +import static com.yahoo.security.ArrayUtils.toUtf8Bytes; + +/** + * Represents a named key ID comprising an arbitrary (but length-limited) + * sequence of valid UTF-8 bytes. + * + * @author vekterli + */ +public class KeyId { + + // Max length MUST be possible to fit in an unsigned byte; see SealedSharedKey token encoding/decoding. + public static final int MAX_KEY_ID_UTF8_LENGTH = 255; + + private final byte[] keyIdBytes; + + private KeyId(byte[] keyIdBytes) { + if (keyIdBytes.length > MAX_KEY_ID_UTF8_LENGTH) { + throw new IllegalArgumentException("Key ID is too large to be encoded (max is %d, got %d)" + .formatted(MAX_KEY_ID_UTF8_LENGTH, keyIdBytes.length)); + } + verifyByteStringRoundtripsAsValidUtf8(keyIdBytes); + this.keyIdBytes = keyIdBytes; + } + + /** + * Construct a KeyId containing the given sequence of bytes. + * + * @param keyIdBytes array of valid UTF-8 bytes. May be zero-length, but not null. + * Note: to avoid accidental mutations, the key bytes are deep-copied. + * @return a new KeyId instance + */ + public static KeyId ofBytes(byte[] keyIdBytes) { + Objects.requireNonNull(keyIdBytes); + return new KeyId(Arrays.copyOf(keyIdBytes, keyIdBytes.length)); + } + + /** + * Construct a KeyId containing the UTF-8 byte representation of the given string. + * + * @param keyId a string whose UTF-8 byte representation will be the key ID. May be + * zero-length but not null. + * @return a new KeyId instance + */ + public static KeyId ofString(String keyId) { + Objects.requireNonNull(keyId); + return new KeyId(toUtf8Bytes(keyId)); + } + + /** + * @return the raw backing byte array. <strong>Must therefore not be mutated.</strong> + */ + public byte[] asBytes() { return keyIdBytes; } + + public String asString() { return fromUtf8Bytes(keyIdBytes); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + KeyId keyId = (KeyId) o; + return Arrays.equals(keyIdBytes, keyId.keyIdBytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(keyIdBytes); + } + + @Override + public String toString() { + return "KeyId(%s)".formatted(asString()); + } + + private static void verifyByteStringRoundtripsAsValidUtf8(byte[] byteStr) { + String asStr = fromUtf8Bytes(byteStr); // Replaces bad chars with a placeholder + byte[] asBytes = toUtf8Bytes(asStr); + if (!Arrays.equals(byteStr, asBytes)) { + throw new IllegalArgumentException("Key ID is not valid normalized UTF-8"); + } + } + +} 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 fe1be85539c..a921b3baf87 100644 --- a/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java +++ b/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java @@ -19,20 +19,14 @@ import static com.yahoo.security.ArrayUtils.toUtf8Bytes; * This token representation is expected to be used as a convenient serialization * form when communicating shared keys. */ -public record SealedSharedKey(byte[] keyId, byte[] enc, byte[] ciphertext) { +public record SealedSharedKey(KeyId 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; - public static final int MAX_KEY_ID_UTF8_LENGTH = 255; /** Encryption context for v1 tokens is always a 32-byte X25519 public key */ public static final int MAX_ENC_CONTEXT_LENGTH = 255; public SealedSharedKey { - if (keyId.length > MAX_KEY_ID_UTF8_LENGTH) { - throw new IllegalArgumentException("Key ID is too large to be encoded (max is %d, got %d)" - .formatted(MAX_KEY_ID_UTF8_LENGTH, keyId.length)); - } - verifyByteStringRoundtripsAsValidUtf8(keyId); if (enc.length > MAX_ENC_CONTEXT_LENGTH) { throw new IllegalArgumentException("Encryption context is too large to be encoded (max is %d, got %d)" .formatted(MAX_ENC_CONTEXT_LENGTH, enc.length)); @@ -44,11 +38,12 @@ public record SealedSharedKey(byte[] keyId, byte[] enc, byte[] ciphertext) { * reconstruct the SealedSharedKey instance when passed verbatim to fromTokenString(). */ public String toTokenString() { + byte[] keyIdBytes = keyId.asBytes(); // u8 token version || u8 length(key id) || key id || u8 length(enc) || enc || ciphertext - ByteBuffer encoded = ByteBuffer.allocate(1 + 1 + keyId.length + 1 + enc.length + ciphertext.length); + ByteBuffer encoded = ByteBuffer.allocate(1 + 1 + keyIdBytes.length + 1 + enc.length + ciphertext.length); encoded.put((byte)CURRENT_TOKEN_VERSION); - encoded.put((byte)keyId.length); - encoded.put(keyId); + encoded.put((byte)keyIdBytes.length); + encoded.put(keyIdBytes); encoded.put((byte)enc.length); encoded.put(enc); encoded.put(ciphertext); @@ -76,24 +71,15 @@ public record SealedSharedKey(byte[] keyId, byte[] enc, byte[] ciphertext) { .formatted(CURRENT_TOKEN_VERSION, version)); } int keyIdLen = Byte.toUnsignedInt(decoded.get()); - byte[] keyId = new byte[keyIdLen]; - decoded.get(keyId); - verifyByteStringRoundtripsAsValidUtf8(keyId); + byte[] keyIdBytes = new byte[keyIdLen]; + decoded.get(keyIdBytes); int encLen = Byte.toUnsignedInt(decoded.get()); byte[] enc = new byte[encLen]; decoded.get(enc); byte[] ciphertext = new byte[decoded.remaining()]; decoded.get(ciphertext); - return new SealedSharedKey(keyId, enc, ciphertext); - } - - private static void verifyByteStringRoundtripsAsValidUtf8(byte[] byteStr) { - String asStr = fromUtf8Bytes(byteStr); // Replaces bad chars with a placeholder - byte[] asBytes = toUtf8Bytes(asStr); - if (!Arrays.equals(byteStr, asBytes)) { - throw new IllegalArgumentException("Key ID is not valid normalized UTF-8"); - } + return new SealedSharedKey(KeyId.ofBytes(keyIdBytes), enc, ciphertext); } public int tokenVersion() { return CURRENT_TOKEN_VERSION; } 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 47936dab114..8a1a7dd3688 100644 --- a/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java +++ b/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java @@ -60,17 +60,17 @@ public class SharedKeyGenerator { } } - public static SecretSharedKey generateForReceiverPublicKey(PublicKey receiverPublicKey, byte[] keyId) { + public static SecretSharedKey generateForReceiverPublicKey(PublicKey receiverPublicKey, KeyId keyId) { var secretKey = generateRandomSecretAesKey(); // We protect the integrity of the key ID by passing it as AAD. - var sealed = HPKE.sealBase((XECPublicKey) receiverPublicKey, EMPTY_BYTES, keyId, secretKey.getEncoded()); + var sealed = HPKE.sealBase((XECPublicKey) receiverPublicKey, EMPTY_BYTES, keyId.asBytes(), secretKey.getEncoded()); var sealedSharedKey = new SealedSharedKey(keyId, sealed.enc(), sealed.ciphertext()); return new SecretSharedKey(secretKey, sealedSharedKey); } public static SecretSharedKey fromSealedKey(SealedSharedKey sealedKey, PrivateKey receiverPrivateKey) { byte[] secretKeyBytes = HPKE.openBase(sealedKey.enc(), (XECPrivateKey) receiverPrivateKey, - EMPTY_BYTES, sealedKey.keyId(), sealedKey.ciphertext()); + EMPTY_BYTES, sealedKey.keyId().asBytes(), sealedKey.ciphertext()); return new SecretSharedKey(new SecretKeySpec(secretKeyBytes, "AES"), sealedKey); } |