diff --git a/src/java.base/share/classes/com/sun/crypto/provider/DHKEM.java b/src/java.base/share/classes/com/sun/crypto/provider/DHKEM.java index b27320ed24b6b..c7372a4c2c8a0 100644 --- a/src/java.base/share/classes/com/sun/crypto/provider/DHKEM.java +++ b/src/java.base/share/classes/com/sun/crypto/provider/DHKEM.java @@ -26,26 +26,51 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.Serial; import java.math.BigInteger; -import java.security.*; -import java.security.interfaces.ECKey; +import java.security.AsymmetricKey; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.PublicKey; +import java.security.SecureRandom; import java.security.interfaces.ECPublicKey; -import java.security.interfaces.XECKey; import java.security.interfaces.XECPublicKey; -import java.security.spec.*; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.NamedParameterSpec; +import java.security.spec.XECPrivateKeySpec; +import java.security.spec.XECPublicKeySpec; import java.util.Arrays; import java.util.Objects; -import javax.crypto.*; -import javax.crypto.spec.SecretKeySpec; +import javax.crypto.DecapsulateException; +import javax.crypto.KDF; +import javax.crypto.KEM; +import javax.crypto.KEMSpi; +import javax.crypto.KeyAgreement; +import javax.crypto.SecretKey; import javax.crypto.spec.HKDFParameterSpec; +import javax.crypto.spec.SecretKeySpec; import sun.security.jca.JCAUtil; -import sun.security.util.*; - -import jdk.internal.access.SharedSecrets; +import sun.security.util.ArrayUtil; +import sun.security.util.CurveDB; +import sun.security.util.ECUtil; +import sun.security.util.InternalPrivateKey; +import sun.security.util.NamedCurve; +import sun.security.util.SliceableSecretKey; // Implementing DHKEM defined inside https://www.rfc-editor.org/rfc/rfc9180.html, -// without the AuthEncap and AuthDecap functions public class DHKEM implements KEMSpi { private static final byte[] KEM = new byte[] @@ -65,80 +90,86 @@ public class DHKEM implements KEMSpi { private static final byte[] EMPTY = new byte[0]; private record Handler(Params params, SecureRandom secureRandom, - PrivateKey skR, PublicKey pkR) + PrivateKey skS, PublicKey pkS, // sender keys + PrivateKey skR, PublicKey pkR) // receiver keys implements EncapsulatorSpi, DecapsulatorSpi { @Override public KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm) { - Objects.checkFromToIndex(from, to, params.Nsecret); + Objects.checkFromToIndex(from, to, params.nsecret); Objects.requireNonNull(algorithm, "null algorithm"); KeyPair kpE = params.generateKeyPair(secureRandom); PrivateKey skE = kpE.getPrivate(); PublicKey pkE = kpE.getPublic(); - byte[] pkEm = params.SerializePublicKey(pkE); - byte[] pkRm = params.SerializePublicKey(pkR); - byte[] kem_context = concat(pkEm, pkRm); - byte[] key = null; + byte[] pkEm = params.serializePublicKey(pkE); + byte[] pkRm = params.serializePublicKey(pkR); try { - byte[] dh = params.DH(skE, pkR); - key = params.ExtractAndExpand(dh, kem_context); - return new KEM.Encapsulated( - new SecretKeySpec(key, from, to - from, algorithm), - pkEm, null); + SecretKey key; + if (skS == null) { + byte[] kem_context = concat(pkEm, pkRm); + key = params.deriveKey(algorithm, from, to, kem_context, + params.dh(skE, pkR)); + } else { + byte[] pkSm = params.serializePublicKey(pkS); + byte[] kem_context = concat(pkEm, pkRm, pkSm); + key = params.deriveKey(algorithm, from, to, kem_context, + params.dh(skE, pkR), params.dh(skS, pkR)); + } + return new KEM.Encapsulated(key, pkEm, null); + } catch (UnsupportedOperationException e) { + throw e; } catch (Exception e) { throw new ProviderException("internal error", e); - } finally { - // `key` has been cloned into the `SecretKeySpec` within the - // returned `KEM.Encapsulated`, so it can now be cleared. - if (key != null) { - Arrays.fill(key, (byte)0); - } } } @Override public SecretKey engineDecapsulate(byte[] encapsulation, int from, int to, String algorithm) throws DecapsulateException { - Objects.checkFromToIndex(from, to, params.Nsecret); + Objects.checkFromToIndex(from, to, params.nsecret); Objects.requireNonNull(algorithm, "null algorithm"); Objects.requireNonNull(encapsulation, "null encapsulation"); - if (encapsulation.length != params.Npk) { + if (encapsulation.length != params.npk) { throw new DecapsulateException("incorrect encapsulation size"); } - byte[] key = null; try { - PublicKey pkE = params.DeserializePublicKey(encapsulation); - byte[] dh = params.DH(skR, pkE); - byte[] pkRm = params.SerializePublicKey(pkR); - byte[] kem_context = concat(encapsulation, pkRm); - key = params.ExtractAndExpand(dh, kem_context); - return new SecretKeySpec(key, from, to - from, algorithm); + PublicKey pkE = params.deserializePublicKey(encapsulation); + byte[] pkRm = params.serializePublicKey(pkR); + if (pkS == null) { + byte[] kem_context = concat(encapsulation, pkRm); + return params.deriveKey(algorithm, from, to, kem_context, + params.dh(skR, pkE)); + } else { + byte[] pkSm = params.serializePublicKey(pkS); + byte[] kem_context = concat(encapsulation, pkRm, pkSm); + return params.deriveKey(algorithm, from, to, kem_context, + params.dh(skR, pkE), params.dh(skR, pkS)); + } + } catch (UnsupportedOperationException e) { + throw e; } catch (IOException | InvalidKeyException e) { throw new DecapsulateException("Cannot decapsulate", e); } catch (Exception e) { throw new ProviderException("internal error", e); - } finally { - if (key != null) { - Arrays.fill(key, (byte)0); - } } } @Override public int engineSecretSize() { - return params.Nsecret; + return params.nsecret; } @Override public int engineEncapsulationSize() { - return params.Npk; + return params.npk; } } // Not really a random. For KAT test only. It generates key pair from ikm. public static class RFC9180DeriveKeyPairSR extends SecureRandom { - static final long serialVersionUID = 0L; + @Serial + private static final long serialVersionUID = 0L; private final byte[] ikm; @@ -147,7 +178,7 @@ public RFC9180DeriveKeyPairSR(byte[] ikm) { this.ikm = ikm; } - public KeyPair derive(Params params) { + private KeyPair derive(Params params) { try { return params.deriveKeyPair(ikm); } catch (Exception e) { @@ -183,9 +214,9 @@ private enum Params { ; private final int kem_id; - private final int Nsecret; - private final int Nsk; - private final int Npk; + private final int nsecret; + private final int nsk; + private final int npk; private final String kaAlgorithm; private final String keyAlgorithm; private final AlgorithmParameterSpec spec; @@ -193,18 +224,18 @@ private enum Params { private final byte[] suiteId; - Params(int kem_id, int Nsecret, int Nsk, int Npk, + Params(int kem_id, int nsecret, int nsk, int npk, String kaAlgorithm, String keyAlgorithm, AlgorithmParameterSpec spec, String hkdfAlgorithm) { this.kem_id = kem_id; this.spec = spec; - this.Nsecret = Nsecret; - this.Nsk = Nsk; - this.Npk = Npk; + this.nsecret = nsecret; + this.nsk = nsk; + this.npk = npk; this.kaAlgorithm = kaAlgorithm; this.keyAlgorithm = keyAlgorithm; this.hkdfAlgorithm = hkdfAlgorithm; - suiteId = concat(KEM, I2OSP(kem_id, 2)); + suiteId = concat(KEM, i2OSP(kem_id, 2)); } private boolean isEC() { @@ -224,18 +255,18 @@ private KeyPair generateKeyPair(SecureRandom sr) { } } - private byte[] SerializePublicKey(PublicKey k) { + private byte[] serializePublicKey(PublicKey k) { if (isEC()) { ECPoint w = ((ECPublicKey) k).getW(); return ECUtil.encodePoint(w, ((NamedCurve) spec).getCurve()); } else { byte[] uArray = ((XECPublicKey) k).getU().toByteArray(); ArrayUtil.reverse(uArray); - return Arrays.copyOf(uArray, Npk); + return Arrays.copyOf(uArray, npk); } } - private PublicKey DeserializePublicKey(byte[] data) + private PublicKey deserializePublicKey(byte[] data) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { KeySpec keySpec; if (isEC()) { @@ -251,29 +282,59 @@ private PublicKey DeserializePublicKey(byte[] data) return KeyFactory.getInstance(keyAlgorithm).generatePublic(keySpec); } - private byte[] DH(PrivateKey skE, PublicKey pkR) + private SecretKey dh(PrivateKey skE, PublicKey pkR) throws NoSuchAlgorithmException, InvalidKeyException { KeyAgreement ka = KeyAgreement.getInstance(kaAlgorithm); ka.init(skE); ka.doPhase(pkR, true); - return ka.generateSecret(); + return ka.generateSecret("Generic"); } - private byte[] ExtractAndExpand(byte[] dh, byte[] kem_context) - throws NoSuchAlgorithmException, InvalidKeyException { - KDF hkdf = KDF.getInstance(hkdfAlgorithm); - SecretKey eae_prk = LabeledExtract(hkdf, suiteId, EAE_PRK, dh); - try { - return LabeledExpand(hkdf, suiteId, eae_prk, SHARED_SECRET, - kem_context, Nsecret); - } finally { - if (eae_prk instanceof SecretKeySpec s) { - SharedSecrets.getJavaxCryptoSpecAccess() - .clearSecretKeySpec(s); + // The final shared secret derivation of either the encapsulator + // or the decapsulator. The key slicing is implemented inside. + // Throws UOE if a slice of the key cannot be found. + private SecretKey deriveKey(String alg, int from, int to, + byte[] kem_context, SecretKey... dhs) + throws NoSuchAlgorithmException { + if (from == 0 && to == nsecret) { + return extractAndExpand(kem_context, alg, dhs); + } else { + // First get shared secrets in "Generic" and then get a slice + // of it in the requested algorithm. + var fullKey = extractAndExpand(kem_context, "Generic", dhs); + if ("RAW".equalsIgnoreCase(fullKey.getFormat())) { + byte[] km = fullKey.getEncoded(); + if (km == null) { + // Should not happen if format is "RAW" + throw new UnsupportedOperationException("Key extract failed"); + } else { + try { + return new SecretKeySpec(km, from, to - from, alg); + } finally { + Arrays.fill(km, (byte)0); + } + } + } else if (fullKey instanceof SliceableSecretKey ssk) { + return ssk.slice(alg, from, to); + } else { + throw new UnsupportedOperationException("Cannot extract key"); } } } + private SecretKey extractAndExpand(byte[] kem_context, String alg, SecretKey... dhs) + throws NoSuchAlgorithmException { + var kdf = KDF.getInstance(hkdfAlgorithm); + var builder = labeledExtract(suiteId, EAE_PRK); + for (var dh : dhs) builder.addIKM(dh); + try { + return kdf.deriveKey(alg, + labeledExpand(builder, suiteId, SHARED_SECRET, kem_context, nsecret)); + } catch (InvalidAlgorithmParameterException e) { + throw new ProviderException(e); + } + } + private PublicKey getPublicKey(PrivateKey sk) throws InvalidKeyException { if (!(sk instanceof InternalPrivateKey)) { @@ -298,45 +359,37 @@ private PublicKey getPublicKey(PrivateKey sk) // For KAT tests only. See RFC9180DeriveKeyPairSR. public KeyPair deriveKeyPair(byte[] ikm) throws Exception { - KDF hkdf = KDF.getInstance(hkdfAlgorithm); - SecretKey dkp_prk = LabeledExtract(hkdf, suiteId, DKP_PRK, ikm); - try { - if (isEC()) { - NamedCurve curve = (NamedCurve) spec; - BigInteger sk = BigInteger.ZERO; - int counter = 0; - while (sk.signum() == 0 || - sk.compareTo(curve.getOrder()) >= 0) { - if (counter > 255) { - throw new RuntimeException(); - } - byte[] bytes = LabeledExpand(hkdf, suiteId, dkp_prk, - CANDIDATE, I2OSP(counter, 1), Nsk); - // bitmask is defined to be 0xFF for P-256 and P-384, - // and 0x01 for P-521 - if (this == Params.P521) { - bytes[0] = (byte) (bytes[0] & 0x01); - } - sk = new BigInteger(1, (bytes)); - counter = counter + 1; + var kdf = KDF.getInstance(hkdfAlgorithm); + var builder = labeledExtract(suiteId, DKP_PRK).addIKM(ikm); + if (isEC()) { + NamedCurve curve = (NamedCurve) spec; + BigInteger sk = BigInteger.ZERO; + int counter = 0; + while (sk.signum() == 0 || sk.compareTo(curve.getOrder()) >= 0) { + if (counter > 255) { + // So unlucky and should not happen + throw new ProviderException("DeriveKeyPairError"); } - PrivateKey k = DeserializePrivateKey(sk.toByteArray()); - return new KeyPair(getPublicKey(k), k); - } else { - byte[] sk = LabeledExpand(hkdf, suiteId, dkp_prk, SK, EMPTY, - Nsk); - PrivateKey k = DeserializePrivateKey(sk); - return new KeyPair(getPublicKey(k), k); - } - } finally { - if (dkp_prk instanceof SecretKeySpec s) { - SharedSecrets.getJavaxCryptoSpecAccess() - .clearSecretKeySpec(s); + byte[] bytes = kdf.deriveData(labeledExpand(builder, + suiteId, CANDIDATE, i2OSP(counter, 1), nsk)); + // bitmask is defined to be 0xFF for P-256 and P-384, and 0x01 for P-521 + if (this == Params.P521) { + bytes[0] = (byte) (bytes[0] & 0x01); + } + sk = new BigInteger(1, (bytes)); + counter = counter + 1; } + PrivateKey k = deserializePrivateKey(sk.toByteArray()); + return new KeyPair(getPublicKey(k), k); + } else { + byte[] sk = kdf.deriveData(labeledExpand(builder, + suiteId, SK, EMPTY, nsk)); + PrivateKey k = deserializePrivateKey(sk); + return new KeyPair(getPublicKey(k), k); } } - private PrivateKey DeserializePrivateKey(byte[] data) throws Exception { + private PrivateKey deserializePrivateKey(byte[] data) throws Exception { KeySpec keySpec = isEC() ? new ECPrivateKeySpec(new BigInteger(1, (data)), (NamedCurve) spec) : new XECPrivateKeySpec(spec, data); @@ -359,7 +412,22 @@ public EncapsulatorSpi engineNewEncapsulator( throw new InvalidAlgorithmParameterException("no spec needed"); } Params params = paramsFromKey(pk); - return new Handler(params, getSecureRandom(secureRandom), null, pk); + return new Handler(params, getSecureRandom(secureRandom), null, null, null, pk); + } + + // AuthEncap is not public KEM API + public EncapsulatorSpi engineNewAuthEncapsulator(PublicKey pkR, PrivateKey skS, + AlgorithmParameterSpec spec, SecureRandom secureRandom) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (pkR == null || skS == null) { + throw new InvalidKeyException("input key is null"); + } + if (spec != null) { + throw new InvalidAlgorithmParameterException("no spec needed"); + } + Params params = paramsFromKey(pkR); + return new Handler(params, getSecureRandom(secureRandom), + skS, params.getPublicKey(skS), null, pkR); } @Override @@ -372,20 +440,34 @@ public DecapsulatorSpi engineNewDecapsulator(PrivateKey sk, AlgorithmParameterSp throw new InvalidAlgorithmParameterException("no spec needed"); } Params params = paramsFromKey(sk); - return new Handler(params, null, sk, params.getPublicKey(sk)); + return new Handler(params, null, null, null, sk, params.getPublicKey(sk)); + } + + // AuthDecap is not public KEM API + public DecapsulatorSpi engineNewAuthDecapsulator( + PrivateKey skR, PublicKey pkS, AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (skR == null || pkS == null) { + throw new InvalidKeyException("input key is null"); + } + if (spec != null) { + throw new InvalidAlgorithmParameterException("no spec needed"); + } + Params params = paramsFromKey(skR); + return new Handler(params, null, null, pkS, skR, params.getPublicKey(skR)); } - private Params paramsFromKey(Key k) throws InvalidKeyException { - if (k instanceof ECKey eckey) { - if (ECUtil.equals(eckey.getParams(), CurveDB.P_256)) { + private Params paramsFromKey(AsymmetricKey k) throws InvalidKeyException { + var p = k.getParams(); + if (p instanceof ECParameterSpec ecp) { + if (ECUtil.equals(ecp, CurveDB.P_256)) { return Params.P256; - } else if (ECUtil.equals(eckey.getParams(), CurveDB.P_384)) { + } else if (ECUtil.equals(ecp, CurveDB.P_384)) { return Params.P384; - } else if (ECUtil.equals(eckey.getParams(), CurveDB.P_521)) { + } else if (ECUtil.equals(ecp, CurveDB.P_521)) { return Params.P521; } - } else if (k instanceof XECKey xkey - && xkey.getParams() instanceof NamedParameterSpec ns) { + } else if (p instanceof NamedParameterSpec ns) { if (ns.getName().equalsIgnoreCase("X25519")) { return Params.X25519; } else if (ns.getName().equalsIgnoreCase("X448")) { @@ -401,8 +483,11 @@ private static byte[] concat(byte[]... inputs) { return o.toByteArray(); } - private static byte[] I2OSP(int n, int w) { - assert n < 256; + // I2OSP(n, w) as defined in RFC 9180 Section 3. + // In DHKEM and HPKE, number is always <65536 + // and converted to at most 2 bytes. + public static byte[] i2OSP(int n, int w) { + assert n < 65536; assert w == 1 || w == 2; if (w == 1) { return new byte[] { (byte) n }; @@ -411,32 +496,32 @@ private static byte[] I2OSP(int n, int w) { } } - private static SecretKey LabeledExtract(KDF hkdf, byte[] suite_id, - byte[] label, byte[] ikm) throws InvalidKeyException { - SecretKeySpec s = new SecretKeySpec(concat(HPKE_V1, suite_id, label, - ikm), "IKM"); - try { - HKDFParameterSpec spec = - HKDFParameterSpec.ofExtract().addIKM(s).extractOnly(); - return hkdf.deriveKey("Generic", spec); - } catch (InvalidAlgorithmParameterException | - NoSuchAlgorithmException e) { - throw new InvalidKeyException(e.getMessage(), e); - } finally { - SharedSecrets.getJavaxCryptoSpecAccess().clearSecretKeySpec(s); - } + // Create a LabeledExtract builder with labels. + // You can add more IKM and salt into the result. + public static HKDFParameterSpec.Builder labeledExtract( + byte[] suiteId, byte[] label) { + return HKDFParameterSpec.ofExtract() + .addIKM(HPKE_V1).addIKM(suiteId).addIKM(label); } - private static byte[] LabeledExpand(KDF hkdf, byte[] suite_id, - SecretKey prk, byte[] label, byte[] info, int L) - throws InvalidKeyException { - byte[] labeled_info = concat(I2OSP(L, 2), HPKE_V1, suite_id, label, - info); - try { - return hkdf.deriveData(HKDFParameterSpec.expandOnly( - prk, labeled_info, L)); - } catch (InvalidAlgorithmParameterException iape) { - throw new InvalidKeyException(iape.getMessage(), iape); - } + // Create a labeled info from info and labels + private static byte[] labeledInfo( + byte[] suiteId, byte[] label, byte[] info, int length) { + return concat(i2OSP(length, 2), HPKE_V1, suiteId, label, info); + } + + // LabeledExpand from a builder + public static HKDFParameterSpec labeledExpand( + HKDFParameterSpec.Builder builder, + byte[] suiteId, byte[] label, byte[] info, int length) { + return builder.thenExpand( + labeledInfo(suiteId, label, info, length), length); + } + + // LabeledExpand from a prk + public static HKDFParameterSpec labeledExpand( + SecretKey prk, byte[] suiteId, byte[] label, byte[] info, int length) { + return HKDFParameterSpec.expandOnly( + prk, labeledInfo(suiteId, label, info, length), length); } } diff --git a/src/java.base/share/classes/com/sun/crypto/provider/HPKE.java b/src/java.base/share/classes/com/sun/crypto/provider/HPKE.java new file mode 100644 index 0000000000000..eee5f59cc75be --- /dev/null +++ b/src/java.base/share/classes/com/sun/crypto/provider/HPKE.java @@ -0,0 +1,588 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.sun.crypto.provider; + +import sun.security.util.CurveDB; +import sun.security.util.ECUtil; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherSpi; +import javax.crypto.DecapsulateException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KDF; +import javax.crypto.KEM; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.HPKEParameterSpec; +import javax.crypto.spec.IvParameterSpec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.security.AlgorithmParameters; +import java.security.AsymmetricKey; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.NamedParameterSpec; +import java.util.Arrays; + +public class HPKE extends CipherSpi { + + private static final byte[] HPKE = new byte[] + {'H', 'P', 'K', 'E'}; + private static final byte[] SEC = new byte[] + {'s', 'e', 'c'}; + private static final byte[] PSK_ID_HASH = new byte[] + {'p', 's', 'k', '_', 'i', 'd', '_', 'h', 'a', 's', 'h'}; + private static final byte[] INFO_HASH = new byte[] + {'i', 'n', 'f', 'o', '_', 'h', 'a', 's', 'h'}; + private static final byte[] SECRET = new byte[] + {'s', 'e', 'c', 'r', 'e', 't'}; + private static final byte[] EXP = new byte[] + {'e', 'x', 'p'}; + private static final byte[] KEY = new byte[] + {'k', 'e', 'y'}; + private static final byte[] BASE_NONCE = new byte[] + {'b', 'a', 's', 'e', '_', 'n', 'o', 'n', 'c', 'e'}; + + private static final int BEGIN = 1; + private static final int EXPORT_ONLY = 2; // init done with aead_id == 65535 + private static final int ENCRYPT_AND_EXPORT = 3; // int done with AEAD + private static final int AFTER_FINAL = 4; // after doFinal, need reinit internal cipher + + private int state = BEGIN; + private Impl impl; + + @Override + protected void engineSetMode(String mode) throws NoSuchAlgorithmException { + throw new NoSuchAlgorithmException(mode); + } + + @Override + protected void engineSetPadding(String padding) throws NoSuchPaddingException { + throw new NoSuchPaddingException(padding); + } + + @Override + protected int engineGetBlockSize() { + if (state == ENCRYPT_AND_EXPORT || state == AFTER_FINAL) { + return impl.aead.cipher.getBlockSize(); + } else { + return 0; + } + } + + @Override + protected int engineGetOutputSize(int inputLen) { + if (state == ENCRYPT_AND_EXPORT || state == AFTER_FINAL) { + return impl.aead.cipher.getOutputSize(inputLen); + } else { + return 0; + } + } + + @Override + protected byte[] engineGetIV() { + return (state == BEGIN || impl.kemEncaps == null) + ? null : impl.kemEncaps.clone(); + } + + @Override + protected AlgorithmParameters engineGetParameters() { + return null; + } + + @Override + protected void engineInit(int opmode, Key key, SecureRandom random) + throws InvalidKeyException { + throw new InvalidKeyException("HPKEParameterSpec must be provided"); + } + + @Override + protected void engineInit(int opmode, Key key, + AlgorithmParameterSpec params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + impl = new Impl(opmode); + if (!(key instanceof AsymmetricKey ak)) { + throw new InvalidKeyException("Not an asymmetric key"); + } + if (params == null) { + throw new InvalidAlgorithmParameterException( + "HPKEParameterSpec must be provided"); + } else if (params instanceof HPKEParameterSpec hps) { + impl.init(ak, hps, random); + } else { + throw new InvalidAlgorithmParameterException( + "Unsupported params type: " + params.getClass()); + } + if (impl.hasEncrypt()) { + impl.aead.start(impl.opmode, impl.context.k, impl.context.computeNonce()); + state = ENCRYPT_AND_EXPORT; + } else { + state = EXPORT_ONLY; + } + } + + @Override + protected void engineInit(int opmode, Key key, + AlgorithmParameters params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + throw new InvalidKeyException("HPKEParameterSpec must be provided"); + } + + // state is ENCRYPT_AND_EXPORT after this call succeeds + private void maybeReinitInternalCipher() { + if (state == BEGIN) { + throw new IllegalStateException("Illegal state: " + state); + } + if (state == EXPORT_ONLY) { + throw new UnsupportedOperationException(); + } + if (state == AFTER_FINAL) { + impl.aead.start(impl.opmode, impl.context.k, impl.context.computeNonce()); + state = ENCRYPT_AND_EXPORT; + } + } + + @Override + protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { + maybeReinitInternalCipher(); + return impl.aead.cipher.update(input, inputOffset, inputLen); + } + + @Override + protected int engineUpdate(byte[] input, int inputOffset, int inputLen, + byte[] output, int outputOffset) throws ShortBufferException { + maybeReinitInternalCipher(); + return impl.aead.cipher.update( + input, inputOffset, inputLen, output, outputOffset); + } + + @Override + protected void engineUpdateAAD(byte[] src, int offset, int len) { + maybeReinitInternalCipher(); + impl.aead.cipher.updateAAD(src, offset, len); + } + + @Override + protected void engineUpdateAAD(ByteBuffer src) { + maybeReinitInternalCipher(); + impl.aead.cipher.updateAAD(src); + } + + @Override + protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) + throws IllegalBlockSizeException, BadPaddingException { + maybeReinitInternalCipher(); + impl.context.IncrementSeq(); + state = AFTER_FINAL; + if (input == null) { // a bug in doFinal(null, ?, ?) + return impl.aead.cipher.doFinal(); + } else { + return impl.aead.cipher.doFinal(input, inputOffset, inputLen); + } + } + + @Override + protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, + byte[] output, int outputOffset) throws ShortBufferException, + IllegalBlockSizeException, BadPaddingException { + maybeReinitInternalCipher(); + impl.context.IncrementSeq(); + state = AFTER_FINAL; + return impl.aead.cipher.doFinal( + input, inputOffset, inputLen, output, outputOffset); + } + + //@Override + protected SecretKey engineExportKey(String algorithm, byte[] context, int length) { + if (state == BEGIN) { + throw new IllegalStateException("State: " + state); + } else { + return impl.context.exportKey(algorithm, context, length); + } + } + + //@Override + protected byte[] engineExportData(byte[] context, int length) { + if (state == BEGIN) { + throw new IllegalStateException("State: " + state); + } else { + return impl.context.exportData(context, length); + } + } + + private static class AEAD { + final Cipher cipher; + final int nk, nn, nt; + final int id; + public AEAD(int id) throws InvalidAlgorithmParameterException { + this.id = id; + try { + switch (id) { + case HPKEParameterSpec.AEAD_AES_128_GCM -> { + cipher = Cipher.getInstance("AES/GCM/NoPadding"); + nk = 16; + } + case HPKEParameterSpec.AEAD_AES_256_GCM -> { + cipher = Cipher.getInstance("AES/GCM/NoPadding"); + nk = 32; + } + case HPKEParameterSpec.AEAD_CHACHA20_POLY1305 -> { + cipher = Cipher.getInstance("ChaCha20-Poly1305"); + nk = 32; + } + case HPKEParameterSpec.EXPORT_ONLY -> { + cipher = null; + nk = -1; + } + default -> throw new InvalidAlgorithmParameterException( + "Unknown aead_id: " + id); + } + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new ProviderException("Internal error", e); + } + nn = 12; nt = 16; + } + + void start(int opmode, SecretKey key, byte[] nonce) { + try { + if (id == HPKEParameterSpec.AEAD_CHACHA20_POLY1305) { + cipher.init(opmode, key, new IvParameterSpec(nonce)); + } else { + cipher.init(opmode, key, new GCMParameterSpec(nt * 8, nonce)); + } + } catch (InvalidAlgorithmParameterException | InvalidKeyException e) { + throw new ProviderException("Internal error", e); + } + } + } + + private static class Impl { + + final int opmode; + + HPKEParameterSpec params; + Context context; + AEAD aead; + + byte[] suite_id; + String kdfAlg; + int kdfNh; + + // only used on sender side + byte[] kemEncaps; + + class Context { + final SecretKey k; // null if only export + final byte[] base_nonce; + final SecretKey exporter_secret; + + byte[] seq = new byte[aead.nn]; + + public Context(SecretKey sk, byte[] base_nonce, + SecretKey exporter_secret) { + this.k = sk; + this.base_nonce = base_nonce; + this.exporter_secret = exporter_secret; + } + + SecretKey exportKey(String algorithm, byte[] exporter_context, int length) { + if (exporter_context == null) { + throw new IllegalArgumentException("Null exporter_context"); + } + try { + var kdf = KDF.getInstance(kdfAlg); + return kdf.deriveKey(algorithm, DHKEM.labeledExpand( + exporter_secret, suite_id, SEC, exporter_context, length)); + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { + // algorithm not accepted by HKDF, length too big or too small + throw new IllegalArgumentException("Invalid input", e); + } + } + + byte[] exportData(byte[] exporter_context, int length) { + if (exporter_context == null) { + throw new IllegalArgumentException("Null exporter_context"); + } + try { + var kdf = KDF.getInstance(kdfAlg); + return kdf.deriveData(DHKEM.labeledExpand( + exporter_secret, suite_id, SEC, exporter_context, length)); + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { + // algorithm not accepted by HKDF, length too big or too small + throw new IllegalArgumentException("Invalid input", e); + } + } + + private byte[] computeNonce() { + var result = new byte[aead.nn]; + for (var i = 0; i < result.length; i++) { + result[i] = (byte)(seq[i] ^ base_nonce[i]); + } + return result; + } + + private void IncrementSeq() { + for (var i = seq.length - 1; i >= 0; i--) { + if ((seq[i] & 0xff) == 0xff) { + seq[i] = 0; + } else { + seq[i]++; + return; + } + } + // seq >= (1 << (8*aead.Nn)) - 1 when this method is called + throw new ProviderException("MessageLimitReachedError"); + } + } + + public Impl(int opmode) { + this.opmode = opmode; + } + + public boolean hasEncrypt() { + return params.aead_id() != 65535; + } + + // Section 7.2.1 of RFC 9180 has restrictions on size of psk, psk_id, + // info, and exporter_context (~2^61 for HMAC-SHA256 and ~2^125 for + // HMAC-SHA384 and HMAC-SHA512). This method does not pose any + // restrictions. + public void init(AsymmetricKey key, HPKEParameterSpec p, SecureRandom rand) + throws InvalidKeyException, InvalidAlgorithmParameterException { + if (opmode != Cipher.ENCRYPT_MODE && opmode != Cipher.DECRYPT_MODE) { + throw new UnsupportedOperationException( + "Can only be used for encryption and decryption"); + } + setParams(p); + SecretKey shared_secret; + if (opmode == Cipher.ENCRYPT_MODE) { + if (!(key instanceof PublicKey pk)) { + throw new InvalidKeyException( + "Cannot encrypt with private key"); + } + if (p.encapsulation() != null) { + throw new InvalidAlgorithmParameterException( + "Must not provide key encapsulation message on sender side"); + } + checkMatch(false, pk, params.kem_id()); + KEM.Encapsulated enc; + switch (p.authKey()) { + case null -> { + var e = kem().newEncapsulator(pk, rand); + enc = e.encapsulate(); + } + case PrivateKey skS -> { + checkMatch(true, skS, params.kem_id()); + // AuthEncap not public KEM API but it's internally supported + var e = new DHKEM().engineNewAuthEncapsulator(pk, skS, null, rand); + enc = e.engineEncapsulate(0, e.engineSecretSize(), "Generic"); + } + default -> throw new InvalidAlgorithmParameterException( + "Cannot auth with public key"); + } + kemEncaps = enc.encapsulation(); + shared_secret = enc.key(); + } else { + if (!(key instanceof PrivateKey sk)) { + throw new InvalidKeyException("Cannot decrypt with public key"); + } + checkMatch(false, sk, params.kem_id()); + try { + var encap = p.encapsulation(); + if (encap == null) { + throw new InvalidAlgorithmParameterException( + "Must provide key encapsulation message on recipient side"); + } + switch (p.authKey()) { + case null -> { + var d = kem().newDecapsulator(sk); + shared_secret = d.decapsulate(encap); + } + case PublicKey pkS -> { + checkMatch(true, pkS, params.kem_id()); + // AuthDecap not public KEM API but it's internally supported + var d = new DHKEM().engineNewAuthDecapsulator(sk, pkS, null); + shared_secret = d.engineDecapsulate( + encap, 0, d.engineSecretSize(), "Generic"); + } + default -> throw new InvalidAlgorithmParameterException( + "Cannot auth with private key"); + } + } catch (DecapsulateException e) { + throw new InvalidAlgorithmParameterException(e); + } + } + + var usePSK = usePSK(params.psk()); + int mode = params.authKey() == null ? (usePSK ? 1 : 0) : (usePSK ? 3 : 2); + context = keySchedule(mode, shared_secret, + params.info(), + params.psk(), + params.psk_id()); + } + + private static void checkMatch(boolean inSpec, AsymmetricKey k, int kem_id) + throws InvalidKeyException, InvalidAlgorithmParameterException { + var p = k.getParams(); + switch (p) { + case ECParameterSpec ecp -> { + if ((!ECUtil.equals(ecp, CurveDB.P_256) + || kem_id != HPKEParameterSpec.KEM_DHKEM_P_256_HKDF_SHA256) + && (!ECUtil.equals(ecp, CurveDB.P_384) + || kem_id != HPKEParameterSpec.KEM_DHKEM_P_384_HKDF_SHA384) + && (!ECUtil.equals(ecp, CurveDB.P_521) + || kem_id != HPKEParameterSpec.KEM_DHKEM_P_521_HKDF_SHA512)) { + var name = ECUtil.getCurveName(ecp); + throw new InvalidAlgorithmParameterException( + name + " does not match " + kem_id); + } + } + case NamedParameterSpec ns -> { + var name = ns.getName(); + if ((!name.equalsIgnoreCase("x25519") + || kem_id != HPKEParameterSpec.KEM_DHKEM_X25519_HKDF_SHA256) + && (!name.equalsIgnoreCase("x448") + || kem_id != HPKEParameterSpec.KEM_DHKEM_X448_HKDF_SHA512)) { + throw new InvalidAlgorithmParameterException( + name + " does not match " + kem_id); + } + } + case null, default -> { + var msg = k.getClass() + " does not match " + kem_id; + if (inSpec) { + throw new InvalidAlgorithmParameterException(msg); + } else { + throw new InvalidKeyException(msg); + } + } + } + } + + private KEM kem() { + try { + return KEM.getInstance("DHKEM"); + } catch (NoSuchAlgorithmException e) { + throw new ProviderException("Internal error", e); + } + } + + private void setParams(HPKEParameterSpec p) + throws InvalidAlgorithmParameterException { + params = p; + suite_id = concat( + HPKE, + DHKEM.i2OSP(params.kem_id(), 2), + DHKEM.i2OSP(params.kdf_id(), 2), + DHKEM.i2OSP(params.aead_id(), 2)); + switch (params.kdf_id()) { + case HPKEParameterSpec.KDF_HKDF_SHA256 -> { + kdfAlg = "HKDF-SHA256"; + kdfNh = 32; + } + case HPKEParameterSpec.KDF_HKDF_SHA384 -> { + kdfAlg = "HKDF-SHA384"; + kdfNh = 48; + } + case HPKEParameterSpec.KDF_HKDF_SHA512 -> { + kdfAlg = "HKDF-SHA512"; + kdfNh = 64; + } + default -> throw new InvalidAlgorithmParameterException( + "Unsupported kdf_id: " + params.kdf_id()); + } + aead = new AEAD(params.aead_id()); + } + + private Context keySchedule(int mode, + SecretKey shared_secret, + byte[] info, + SecretKey psk, + byte[] psk_id) { + try { + var psk_id_hash_x = DHKEM.labeledExtract(suite_id, PSK_ID_HASH) + .addIKM(psk_id).extractOnly(); + var info_hash_x = DHKEM.labeledExtract(suite_id, INFO_HASH) + .addIKM(info).extractOnly(); + + // deriveData must and can be called because all info to + // thw builder are just byte arrays. Any KDF impl can handle this. + var kdf = KDF.getInstance(kdfAlg); + var key_schedule_context = concat(new byte[]{(byte) mode}, + kdf.deriveData(psk_id_hash_x), + kdf.deriveData(info_hash_x)); + + var secret_x_builder = DHKEM.labeledExtract(suite_id, SECRET); + if (psk != null) { + secret_x_builder.addIKM(psk); + } + secret_x_builder.addSalt(shared_secret); + var secret_x = kdf.deriveKey("Generic", secret_x_builder.extractOnly()); + + // A new KDF object must be created because secret_x_builder + // might contain provider-specific keys which the previous + // KDF (provider already chosen) cannot handle. + kdf = KDF.getInstance(kdfAlg); + var exporter_secret = kdf.deriveKey("Generic", DHKEM.labeledExpand( + secret_x, suite_id, EXP, key_schedule_context, kdfNh)); + + if (hasEncrypt()) { + // ChaCha20-Poly1305 does not care about algorithm name + var key = kdf.deriveKey("AES", DHKEM.labeledExpand(secret_x, + suite_id, KEY, key_schedule_context, aead.nk)); + // deriveData must be called because we need to increment nonce + var base_nonce = kdf.deriveData(DHKEM.labeledExpand(secret_x, + suite_id, BASE_NONCE, key_schedule_context, aead.nn)); + return new Context(key, base_nonce, exporter_secret); + } else { + return new Context(null, null, exporter_secret); + } + } catch (InvalidAlgorithmParameterException + | NoSuchAlgorithmException | UnsupportedOperationException e) { + throw new ProviderException("Internal error", e); + } + } + } + + private static boolean usePSK(SecretKey psk) { + return psk != null; + } + + private static byte[] concat(byte[]... inputs) { + var o = new ByteArrayOutputStream(); + Arrays.stream(inputs).forEach(o::writeBytes); + return o.toByteArray(); + } +} diff --git a/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java b/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java index 22d5f17c6e0fb..4b38bd55809eb 100644 --- a/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java +++ b/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java @@ -371,6 +371,8 @@ void putEntries() { ps("Cipher", "PBEWithHmacSHA512/256AndAES_256", "com.sun.crypto.provider.PBES2Core$HmacSHA512_256AndAES_256"); + ps("Cipher", "HPKE", "com.sun.crypto.provider.HPKE"); + /* * Key(pair) Generator engines */ diff --git a/src/java.base/share/classes/javax/crypto/spec/HPKEParameterSpec.java b/src/java.base/share/classes/javax/crypto/spec/HPKEParameterSpec.java new file mode 100644 index 0000000000000..6776ddcdb75a4 --- /dev/null +++ b/src/java.base/share/classes/javax/crypto/spec/HPKEParameterSpec.java @@ -0,0 +1,443 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package javax.crypto.spec; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.security.AsymmetricKey; +import java.security.Key; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.Objects; + +/** + * This immutable class specifies the set of parameters used with a {@code Cipher} for the + * Hybrid Public Key Encryption + * (HPKE) algorithm. HPKE is a public key encryption scheme for encrypting + * arbitrary-sized plaintexts with a recipient's public key. It combines a key + * encapsulation mechanism (KEM), a key derivation function (KDF), and an + * authenticated encryption with additional data (AEAD) cipher. + *
+ * The + * standard algorithm name for the cipher is "HPKE". Unlike most other + * ciphers, HPKE is not expressed as a transformation string of the form + * "algorithm/mode/padding". Therefore, the argument to {@code Cipher.getInstance} + * must be the single algorithm name "HPKE". + *
+ * In HPKE, the sender's {@code Cipher} is always initialized with the + * recipient's public key in {@linkplain Cipher#ENCRYPT_MODE encrypt mode}, + * while the recipient's {@code Cipher} object is initialized with its own + * private key in {@linkplain Cipher#DECRYPT_MODE decrypt mode}. + *
+ * An {@code HPKEParameterSpec} object must be provided at HPKE + * {@linkplain Cipher#init(int, Key, AlgorithmParameterSpec) cipher initialization}. + *
+ * The {@link #of(int, int, int)} static method returns an {@code HPKEParameterSpec} + * object with the specified KEM, KDF, and AEAD algorithm identifiers. + * The terms "KEM algorithm identifiers", "KDF algorithm identifiers", and + * "AEAD algorithm identifiers" refer to their respective numeric values + * (specifically, {@code kem_id}, {@code kdf_id}, and {@code aead_id}) as + * defined in Section 7 + * of RFC 9180 and maintained on the + * IANA HPKE page. + *
+ * Once an {@code HPKEParameterSpec} object is created, additional methods + * are available to generate new {@code HPKEParameterSpec} objects with + * different features: + *
+ * If an HPKE cipher is {@linkplain Cipher#init(int, Key) initialized without + * parameters}, an {@code InvalidKeyException} is thrown. + *
+ * At HPKE cipher initialization, if no HPKE implementation supports the + * provided key type, an {@code InvalidKeyException} is thrown. If the provided + * {@code HPKEParameterSpec} is not accepted by any HPKE implementation, + * an {@code InvalidAlgorithmParameterException} is thrown. For example: + *
+ * This example shows a sender and a recipient using HPKE to securely exchange + * messages with an X25519 key pair. + * {@snippet lang=java class="PackageSnippets" region="hpke-spec-example"} + * + * @implNote This class defines constants for some of the standard algorithm + * identifiers such as {@link #KEM_DHKEM_P_256_HKDF_SHA256}, + * {@link #KDF_HKDF_SHA256}, and {@link #AEAD_AES_128_GCM}. An HPKE {@code Cipher} + * implementation may support all, some, or none of the algorithm identifiers + * defined here. An implementation may also support additional identifiers not + * listed here, including private or experimental values. + * + * @spec https://www.rfc-editor.org/info/rfc9180 + * RFC 9180: Hybrid Public Key Encryption + * @spec security/standard-names.html + * Java Security Standard Algorithm Names + * @since 26 + */ +public final class HPKEParameterSpec implements AlgorithmParameterSpec { + + /** + * KEM algorithm identifier for DHKEM(P-256, HKDF-SHA256) as defined in RFC 9180. + */ + public static final int KEM_DHKEM_P_256_HKDF_SHA256 = 0x10; + + /** + * KEM algorithm identifier for DHKEM(P-384, HKDF-SHA384) as defined in RFC 9180. + */ + public static final int KEM_DHKEM_P_384_HKDF_SHA384 = 0x11; + + /** + * KEM algorithm identifier for DHKEM(P-521, HKDF-SHA512) as defined in RFC 9180. + */ + public static final int KEM_DHKEM_P_521_HKDF_SHA512 = 0x12; + + /** + * KEM algorithm identifier for DHKEM(X25519, HKDF-SHA256) as defined in RFC 9180. + */ + public static final int KEM_DHKEM_X25519_HKDF_SHA256 = 0x20; + + /** + * KEM algorithm identifier for DHKEM(X448, HKDF-SHA512) as defined in RFC 9180. + */ + public static final int KEM_DHKEM_X448_HKDF_SHA512 = 0x21; + + /** + * KDF algorithm identifier for HKDF-SHA256 as defined in RFC 9180. + */ + public static final int KDF_HKDF_SHA256 = 0x1; + + /** + * KDF algorithm identifier for HKDF-SHA384 as defined in RFC 9180. + */ + public static final int KDF_HKDF_SHA384 = 0x2; + + /** + * KDF algorithm identifier for HKDF-SHA512 as defined in RFC 9180. + */ + public static final int KDF_HKDF_SHA512 = 0x3; + + /** + * AEAD algorithm identifier for AES-128-GCM as defined in RFC 9180. + */ + public static final int AEAD_AES_128_GCM = 0x1; + + /** + * AEAD algorithm identifier for AES-256-GCM as defined in RFC 9180. + */ + public static final int AEAD_AES_256_GCM = 0x2; + + /** + * AEAD algorithm identifier for ChaCha20Poly1305 as defined in RFC 9180. + */ + public static final int AEAD_CHACHA20_POLY1305 = 0x3; + + /** + * AEAD algorithm identifier for Export-only as defined in RFC 9180. + */ + public static final int EXPORT_ONLY = 0xffff; + + private final int kem_id; + private final int kdf_id; + private final int aead_id; + private final byte[] info; // never null, can be empty + private final SecretKey psk; // null if not used + private final byte[] psk_id; // never null, can be empty + private final AsymmetricKey kS; // null if not used + private final byte[] encapsulation; // null if none + + // Note: this constructor does not clone array arguments. + private HPKEParameterSpec(int kem_id, int kdf_id, int aead_id, byte[] info, + SecretKey psk, byte[] psk_id, AsymmetricKey kS, byte[] encapsulation) { + this.kem_id = kem_id; + this.kdf_id = kdf_id; + this.aead_id = aead_id; + this.info = info; + this.psk = psk; + this.psk_id = psk_id; + this.kS = kS; + this.encapsulation = encapsulation; + } + + /** + * A factory method to create a new {@code HPKEParameterSpec} object with + * specified KEM, KDF, and AEAD algorithm identifiers in {@code mode_base} + * mode with an empty {@code info}. + * + * @param kem_id algorithm identifier for KEM, must be between 0 and 65535 (inclusive) + * @param kdf_id algorithm identifier for KDF, must be between 0 and 65535 (inclusive) + * @param aead_id algorithm identifier for AEAD, must be between 0 and 65535 (inclusive) + * @return a new {@code HPKEParameterSpec} object + * @throws IllegalArgumentException if any input value + * is out of range (must be between 0 and 65535, inclusive). + */ + public static HPKEParameterSpec of(int kem_id, int kdf_id, int aead_id) { + if (kem_id < 0 || kem_id > 65535) { + throw new IllegalArgumentException("Invalid kem_id: " + kem_id); + } + if (kdf_id < 0 || kdf_id > 65535) { + throw new IllegalArgumentException("Invalid kdf_id: " + kdf_id); + } + if (aead_id < 0 || aead_id > 65535) { + throw new IllegalArgumentException("Invalid aead_id: " + aead_id); + } + return new HPKEParameterSpec(kem_id, kdf_id, aead_id, + new byte[0], null, new byte[0], null, null); + } + + /** + * Creates a new {@code HPKEParameterSpec} object with the specified + * {@code info} value. + *
+ * For interoperability, RFC 9180 Section 7.2.1 recommends limiting + * this value to a maximum of 64 bytes. + * + * @param info application-supplied information. + * The contents of the array are copied to protect + * against subsequent modification. + * @return a new {@code HPKEParameterSpec} object + * @throws NullPointerException if {@code info} is {@code null} + * @throws IllegalArgumentException if {@code info} is empty. + */ + public HPKEParameterSpec withInfo(byte[] info) { + Objects.requireNonNull(info); + if (info.length == 0) { + throw new IllegalArgumentException("info is empty"); + } + return new HPKEParameterSpec(kem_id, kdf_id, aead_id, + info.clone(), psk, psk_id, kS, encapsulation); + } + + /** + * Creates a new {@code HPKEParameterSpec} object with the specified + * {@code psk} and {@code psk_id} values. + *
+ * RFC 9180 Section 5.1.2 requires the PSK MUST have at least 32 bytes + * of entropy. For interoperability, RFC 9180 Section 7.2.1 recommends + * limiting the key size and identifier length to a maximum of 64 bytes. + * + * @param psk pre-shared key + * @param psk_id identifier for PSK. The contents of the array are copied + * to protect against subsequent modification. + * @return a new {@code HPKEParameterSpec} object + * @throws NullPointerException if {@code psk} or {@code psk_id} is {@code null} + * @throws IllegalArgumentException if {@code psk} is shorter than 32 bytes + * or {@code psk_id} is empty + */ + public HPKEParameterSpec withPsk(SecretKey psk, byte[] psk_id) { + Objects.requireNonNull(psk); + Objects.requireNonNull(psk_id); + if (psk_id.length == 0) { + throw new IllegalArgumentException("psk_id is empty"); + } + if ("RAW".equalsIgnoreCase(psk.getFormat())) { + // We can only check when psk is extractable. We can only + // check the length and not the real entropy size + var keyBytes = psk.getEncoded(); + assert keyBytes != null; + Arrays.fill(keyBytes, (byte)0); + if (keyBytes.length < 32) { + throw new IllegalArgumentException("psk is too short"); + } + } + return new HPKEParameterSpec(kem_id, kdf_id, aead_id, + info, psk, psk_id.clone(), kS, encapsulation); + } + + /** + * Creates a new {@code HPKEParameterSpec} object with the specified + * key encapsulation message value that will be used by the recipient. + * + * @param encapsulation the key encapsulation message. + * The contents of the array are copied to protect against + * subsequent modification. + * + * @return a new {@code HPKEParameterSpec} object + * @throws NullPointerException if {@code encapsulation} is {@code null} + */ + public HPKEParameterSpec withEncapsulation(byte[] encapsulation) { + return new HPKEParameterSpec(kem_id, kdf_id, aead_id, + info, psk, psk_id, kS, + Objects.requireNonNull(encapsulation).clone()); + } + + /** + * Creates a new {@code HPKEParameterSpec} object with the specified + * authentication key value. + *
+ * Note: this method does not check whether the KEM algorithm supports
+ * {@code mode_auth} or {@code mode_auth_psk}. If the resulting object is
+ * used to initialize an HPKE cipher with an unsupported mode, an
+ * {@code InvalidAlgorithmParameterException} will be thrown at that time.
+ *
+ * @param kS the authentication key
+ * @return a new {@code HPKEParameterSpec} object
+ * @throws NullPointerException if {@code kS} is {@code null}
+ */
+ public HPKEParameterSpec withAuthKey(AsymmetricKey kS) {
+ return new HPKEParameterSpec(kem_id, kdf_id, aead_id,
+ info, psk, psk_id,
+ Objects.requireNonNull(kS),
+ encapsulation);
+ }
+
+ /**
+ * {@return the algorithm identifier for KEM }
+ */
+ public int kem_id() {
+ return kem_id;
+ }
+
+ /**
+ * {@return the algorithm identifier for KDF }
+ */
+ public int kdf_id() {
+ return kdf_id;
+ }
+
+ /**
+ * {@return the algorithm identifier for AEAD }
+ */
+ public int aead_id() {
+ return aead_id;
+ }
+
+ /**
+ * {@return a copy of the application-supplied information, empty if none}
+ */
+ public byte[] info() {
+ return info.clone();
+ }
+
+ /**
+ * {@return pre-shared key, {@code null} if none}
+ */
+ public SecretKey psk() {
+ return psk;
+ }
+
+ /**
+ * {@return a copy of the identifier for PSK, empty if none}
+ */
+ public byte[] psk_id() {
+ return psk_id.clone();
+ }
+
+ /**
+ * {@return the key for authentication, {@code null} if none}
+ */
+ public AsymmetricKey authKey() {
+ return kS;
+ }
+
+ /**
+ * {@return a copy of the key encapsulation message, {@code null} if none}
+ */
+ public byte[] encapsulation() {
+ return encapsulation == null ? null : encapsulation.clone();
+ }
+
+ @Override
+ public String toString() {
+ return "HPKEParameterSpec{" +
+ "kem_id=" + kem_id +
+ ", kdf_id=" + kdf_id +
+ ", aead_id=" + aead_id +
+ ", info=" + bytesToString(info) +
+ ", " + (psk == null
+ ? (kS == null ? "mode_base" : "mode_auth")
+ : (kS == null ? "mode_psk" : "mode_auth_psk")) + "}";
+ }
+
+ // Returns a human-readable representation of a byte array.
+ private static String bytesToString(byte[] input) {
+ if (input.length == 0) {
+ return "(empty)";
+ } else {
+ for (byte b : input) {
+ if (b < 0x20 || b > 0x7E || b == '"') {
+ // Non-ASCII or control characters are hard to read, and
+ // `"` requires character escaping. If any of these are
+ // present, return only the HEX representation.
+ return HexFormat.of().formatHex(input);
+ }
+ }
+ // Otherwise, all characters are printable and safe.
+ // Return both HEX and ASCII representations.
+ return HexFormat.of().formatHex(input)
+ + " (\"" + new String(input, StandardCharsets.US_ASCII) + "\")";
+ }
+ }
+}
diff --git a/src/java.base/share/classes/javax/crypto/spec/snippet-files/PackageSnippets.java b/src/java.base/share/classes/javax/crypto/spec/snippet-files/PackageSnippets.java
new file mode 100644
index 0000000000000..e4074c1c4a9ae
--- /dev/null
+++ b/src/java.base/share/classes/javax/crypto/spec/snippet-files/PackageSnippets.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+import javax.crypto.Cipher;
+import javax.crypto.spec.HPKEParameterSpec;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.util.Arrays;
+import java.util.HexFormat;
+
+class PackageSnippets {
+ public static void main(String[] args) throws Exception {
+
+ // @start region="hpke-spec-example"
+ // Recipient key pair generation
+ KeyPairGenerator g = KeyPairGenerator.getInstance("X25519");
+ KeyPair kp = g.generateKeyPair();
+
+ // The HPKE sender cipher is initialized with the recipient's public
+ // key and an HPKEParameterSpec using specified algorithm identifiers
+ // and application-supplied info.
+ Cipher senderCipher = Cipher.getInstance("HPKE");
+ HPKEParameterSpec ps = HPKEParameterSpec.of(
+ HPKEParameterSpec.KEM_DHKEM_X25519_HKDF_SHA256,
+ HPKEParameterSpec.KDF_HKDF_SHA256,
+ HPKEParameterSpec.AEAD_AES_128_GCM)
+ .withInfo(HexFormat.of().parseHex("010203040506"));
+ senderCipher.init(Cipher.ENCRYPT_MODE, kp.getPublic(), ps);
+
+ // Retrieve the key encapsulation message (from the KEM step) from
+ // the sender.
+ byte[] kemEncap = senderCipher.getIV();
+
+ // The HPKE recipient cipher is initialized with its own private key,
+ // an HPKEParameterSpec using the same algorithm identifiers as used by
+ // the sender, and the key encapsulation message from the sender.
+ Cipher recipientCipher = Cipher.getInstance("HPKE");
+ HPKEParameterSpec pr = HPKEParameterSpec.of(
+ HPKEParameterSpec.KEM_DHKEM_X25519_HKDF_SHA256,
+ HPKEParameterSpec.KDF_HKDF_SHA256,
+ HPKEParameterSpec.AEAD_AES_128_GCM)
+ .withInfo(HexFormat.of().parseHex("010203040506"))
+ .withEncapsulation(kemEncap);
+ recipientCipher.init(Cipher.DECRYPT_MODE, kp.getPrivate(), pr);
+
+ // Encryption and decryption
+ byte[] msg = "Hello World".getBytes(StandardCharsets.UTF_8);
+ byte[] ct = senderCipher.doFinal(msg);
+ byte[] pt = recipientCipher.doFinal(ct);
+
+ assert Arrays.equals(msg, pt);
+ // @end
+ }
+}
diff --git a/src/java.base/share/classes/sun/security/util/SliceableSecretKey.java b/src/java.base/share/classes/sun/security/util/SliceableSecretKey.java
new file mode 100644
index 0000000000000..4dc3fe0a3e8b0
--- /dev/null
+++ b/src/java.base/share/classes/sun/security/util/SliceableSecretKey.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package sun.security.util;
+
+import javax.crypto.SecretKey;
+
+/**
+ * An interface for SecretKeys that support using its slice as a new
+ * SecretKey.
+ *
+ * This is mainly used by PKCS #11 implementations that support the
+ * EXTRACT_KEY_FROM_KEY mechanism even if the key itself is sensitive
+ * and non-extractable.
+ */
+public interface SliceableSecretKey {
+
+ /**
+ * Returns a slice as a new SecretKey.
+ *
+ * @param alg the new algorithm name
+ * @param from the byte offset of the new key in the full key
+ * @param to the to offset (exclusive) of the new key in the full key
+ * @return the new key
+ * @throws ArrayIndexOutOfBoundsException for improper from
+ * and to values
+ * @throws UnsupportedOperationException if slicing is not supported
+ */
+ SecretKey slice(String alg, int from, int to);
+}
diff --git a/test/jdk/com/sun/crypto/provider/Cipher/HPKE/Compliance.java b/test/jdk/com/sun/crypto/provider/Cipher/HPKE/Compliance.java
new file mode 100644
index 0000000000000..2e10bb23e828f
--- /dev/null
+++ b/test/jdk/com/sun/crypto/provider/Cipher/HPKE/Compliance.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import jdk.test.lib.Asserts;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.HPKEParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.NamedParameterSpec;
+
+import static javax.crypto.spec.HPKEParameterSpec.AEAD_AES_256_GCM;
+import static javax.crypto.spec.HPKEParameterSpec.KDF_HKDF_SHA256;
+import static javax.crypto.spec.HPKEParameterSpec.KEM_DHKEM_X25519_HKDF_SHA256;
+
+/*
+ * @test
+ * @bug 8325448
+ * @library /test/lib
+ * @summary HPKE compliance test
+ */
+public class Compliance {
+ public static void main(String[] args) throws Exception {
+
+ var kp = KeyPairGenerator.getInstance("X25519").generateKeyPair();
+ var info = "info".getBytes(StandardCharsets.UTF_8);
+ var psk = new SecretKeySpec(new byte[32], "ONE");
+ var shortKey = new SecretKeySpec(new byte[31], "ONE");
+ var psk_id = "psk_id".getBytes(StandardCharsets.UTF_8);
+ var emptyKey = new SecretKey() {
+ public String getAlgorithm() { return "GENERIC"; }
+ public String getFormat() { return "RAW"; }
+ public byte[] getEncoded() { return new byte[0]; }
+ };
+
+ // HPKEParameterSpec
+
+ // A typical spec
+ var spec = HPKEParameterSpec.of(
+ KEM_DHKEM_X25519_HKDF_SHA256,
+ KDF_HKDF_SHA256,
+ AEAD_AES_256_GCM);
+ Asserts.assertEQ(spec.kem_id(), KEM_DHKEM_X25519_HKDF_SHA256);
+ Asserts.assertEQ(spec.kdf_id(), KDF_HKDF_SHA256);
+ Asserts.assertEQ(spec.aead_id(), AEAD_AES_256_GCM);
+ Asserts.assertEQ(spec.authKey(), null);
+ Asserts.assertEQ(spec.encapsulation(), null);
+ Asserts.assertEqualsByteArray(spec.info(), new byte[0]);
+ Asserts.assertEQ(spec.psk(), null);
+ Asserts.assertEqualsByteArray(spec.psk_id(), new byte[0]);
+
+ // A fake spec but still valid
+ var specZero = HPKEParameterSpec.of(0, 0, 0);
+ Asserts.assertEQ(specZero.kem_id(), 0);
+ Asserts.assertEQ(specZero.kdf_id(), 0);
+ Asserts.assertEQ(specZero.aead_id(), 0);
+ Asserts.assertEQ(specZero.authKey(), null);
+ Asserts.assertEQ(specZero.encapsulation(), null);
+ Asserts.assertEqualsByteArray(specZero.info(), new byte[0]);
+ Asserts.assertEQ(specZero.psk(), null);
+ Asserts.assertEqualsByteArray(specZero.psk_id(), new byte[0]);
+
+ // identifiers
+ HPKEParameterSpec.of(65535, 65535, 65535);
+ Asserts.assertThrows(IllegalArgumentException.class,
+ () -> HPKEParameterSpec.of(-1, 0, 0));
+ Asserts.assertThrows(IllegalArgumentException.class,
+ () -> HPKEParameterSpec.of(0, -1, 0));
+ Asserts.assertThrows(IllegalArgumentException.class,
+ () -> HPKEParameterSpec.of(0, 0, -1));
+ Asserts.assertThrows(IllegalArgumentException.class,
+ () -> HPKEParameterSpec.of(65536, 0, 0));
+ Asserts.assertThrows(IllegalArgumentException.class,
+ () -> HPKEParameterSpec.of(0, 65536, 0));
+ Asserts.assertThrows(IllegalArgumentException.class,
+ () -> HPKEParameterSpec.of(0, 0, 65536));
+
+ // auth key
+ Asserts.assertTrue(spec.withAuthKey(kp.getPrivate()).authKey() != null);
+ Asserts.assertTrue(spec.withAuthKey(kp.getPublic()).authKey() != null);
+ Asserts.assertThrows(NullPointerException.class, () -> spec.withAuthKey(null));
+
+ // info
+ Asserts.assertEqualsByteArray(spec.withInfo(info).info(), info);
+ Asserts.assertThrows(NullPointerException.class, () -> spec.withInfo(null));
+ Asserts.assertThrows(IllegalArgumentException.class, () -> spec.withInfo(new byte[0]));
+
+ // encapsulation
+ Asserts.assertEqualsByteArray(spec.withEncapsulation(info).encapsulation(), info);
+ Asserts.assertThrows(NullPointerException.class, () -> spec.withEncapsulation(null));
+ Asserts.assertTrue(spec.withEncapsulation(new byte[0]).encapsulation().length == 0); // not emptiness check (yet)
+
+ // psk_id and psk
+ Asserts.assertEqualsByteArray(spec.withPsk(psk, psk_id).psk().getEncoded(), psk.getEncoded());
+ Asserts.assertEqualsByteArray(spec.withPsk(psk, psk_id).psk_id(), psk_id);
+ Asserts.assertThrows(NullPointerException.class, () -> spec.withPsk(psk, null));
+ Asserts.assertThrows(NullPointerException.class, () -> spec.withPsk(null, psk_id));
+ Asserts.assertThrows(NullPointerException.class, () -> spec.withPsk(null, null));
+ Asserts.assertThrows(IllegalArgumentException.class, () -> spec.withPsk(psk, new byte[0]));
+ Asserts.assertThrows(IllegalArgumentException.class, () -> spec.withPsk(emptyKey, psk_id));
+ Asserts.assertThrows(IllegalArgumentException.class, () -> spec.withPsk(shortKey, psk_id));
+
+ // toString
+ Asserts.assertTrue(spec.toString().contains("kem_id=32, kdf_id=1, aead_id=2"));
+ Asserts.assertTrue(spec.toString().contains("info=(empty),"));
+ Asserts.assertTrue(spec.withInfo(new byte[3]).toString().contains("info=000000,"));
+ Asserts.assertTrue(spec.withInfo("info".getBytes(StandardCharsets.UTF_8))
+ .toString().contains("info=696e666f (\"info\"),"));
+ Asserts.assertTrue(spec.withInfo("\"info\"".getBytes(StandardCharsets.UTF_8))
+ .toString().contains("info=22696e666f22,"));
+ Asserts.assertTrue(spec.withInfo("'info'".getBytes(StandardCharsets.UTF_8))
+ .toString().contains("info=27696e666f27 (\"'info'\"),"));
+ Asserts.assertTrue(spec.withInfo("i\\n\\f\\o".getBytes(StandardCharsets.UTF_8))
+ .toString().contains("info=695c6e5c665c6f (\"i\\n\\f\\o\"),"));
+ Asserts.assertTrue(spec.toString().contains("mode_base}"));
+ Asserts.assertTrue(spec.withPsk(psk, psk_id).toString().contains("mode_psk}"));
+ Asserts.assertTrue(spec.withAuthKey(kp.getPrivate()).toString().contains("mode_auth}"));
+ Asserts.assertTrue(spec.withAuthKey(kp.getPrivate()).withPsk(psk, psk_id).toString().contains("mode_auth_psk}"));
+
+ var c1 = Cipher.getInstance("HPKE");
+
+ Asserts.assertThrows(NoSuchAlgorithmException.class, () -> Cipher.getInstance("HPKE/None/NoPadding"));
+
+ // Still at BEGIN, not initialized
+ Asserts.assertEQ(c1.getIV(), null);
+ Asserts.assertEQ(c1.getParameters(), null);
+ Asserts.assertEquals(0, c1.getBlockSize());
+ Asserts.assertThrows(IllegalStateException.class, () -> c1.getOutputSize(100));
+ Asserts.assertThrows(IllegalStateException.class, () -> c1.update(new byte[1]));
+ Asserts.assertThrows(IllegalStateException.class, () -> c1.update(new byte[1], 0, 1));
+ Asserts.assertThrows(IllegalStateException.class, () -> c1.updateAAD(new byte[1]));
+ Asserts.assertThrows(IllegalStateException.class, () -> c1.updateAAD(new byte[1], 0, 1));
+ Asserts.assertThrows(IllegalStateException.class, () -> c1.doFinal());
+ Asserts.assertThrows(IllegalStateException.class, () -> c1.doFinal(new byte[1]));
+ Asserts.assertThrows(IllegalStateException.class, () -> c1.doFinal(new byte[1], 0, 1));
+ Asserts.assertThrows(IllegalStateException.class, () -> c1.doFinal(new byte[1], 0, 1, new byte[1024], 0));
+
+ c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), spec);
+ var encap = c1.getIV();
+
+ // Does not support WRAP and UNWRAP mode
+ Asserts.assertThrows(UnsupportedOperationException.class,
+ () -> c1.init(Cipher.WRAP_MODE, kp.getPublic(), spec));
+ Asserts.assertThrows(UnsupportedOperationException.class,
+ () -> c1.init(Cipher.UNWRAP_MODE, kp.getPublic(), spec));
+
+ // Nulls
+ Asserts.assertThrows(InvalidKeyException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, null, spec));
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), (HPKEParameterSpec) null));
+
+ // Cannot init sender with private key
+ Asserts.assertThrows(InvalidKeyException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPrivate(), spec));
+
+ // Cannot provide key encap msg to sender
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(),
+ spec.withEncapsulation(encap)));
+
+ // Cannot init without HPKEParameterSpec
+ Asserts.assertThrows(InvalidKeyException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic()));
+ Asserts.assertThrows(InvalidKeyException.class,
+ () -> c1.init(Cipher.DECRYPT_MODE, kp.getPrivate()));
+
+ // Cannot init with a spec not HPKEParameterSpec
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(),
+ NamedParameterSpec.X25519));
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.DECRYPT_MODE, kp.getPrivate(),
+ NamedParameterSpec.X25519));
+
+ // Cannot init recipient with public key
+ Asserts.assertThrows(InvalidKeyException.class,
+ () -> c1.init(Cipher.DECRYPT_MODE, kp.getPublic(),
+ spec.withEncapsulation(new byte[32])));
+ // Cannot provide key encap msg to sender
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), spec.withEncapsulation(encap)));
+ // Must provide key encap msg to recipient
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.DECRYPT_MODE, kp.getPrivate(), spec));
+
+ // Unsupported identifiers
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(),
+ HPKEParameterSpec.of(0, KDF_HKDF_SHA256, AEAD_AES_256_GCM)));
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(),
+ HPKEParameterSpec.of(0x200, KDF_HKDF_SHA256, AEAD_AES_256_GCM)));
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(),
+ HPKEParameterSpec.of(KEM_DHKEM_X25519_HKDF_SHA256, 4, AEAD_AES_256_GCM)));
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(),
+ HPKEParameterSpec.of(KEM_DHKEM_X25519_HKDF_SHA256, KDF_HKDF_SHA256, 4)));
+
+ // HPKE
+ checkEncryptDecrypt(kp, spec, spec);
+
+ // extra features
+ var kp2 = KeyPairGenerator.getInstance("X25519").generateKeyPair();
+ checkEncryptDecrypt(kp,
+ spec.withInfo(info),
+ spec.withInfo(info));
+ checkEncryptDecrypt(kp,
+ spec.withPsk(psk, psk_id),
+ spec.withPsk(psk, psk_id));
+ checkEncryptDecrypt(kp,
+ spec.withAuthKey(kp2.getPrivate()),
+ spec.withAuthKey(kp2.getPublic()));
+ checkEncryptDecrypt(kp,
+ spec.withInfo(info).withPsk(psk, psk_id).withAuthKey(kp2.getPrivate()),
+ spec.withInfo(info).withPsk(psk, psk_id).withAuthKey(kp2.getPublic()));
+
+ // wrong keys
+ var kpRSA = KeyPairGenerator.getInstance("RSA").generateKeyPair();
+ var kpEC = KeyPairGenerator.getInstance("EC").generateKeyPair();
+
+ Asserts.assertThrows(InvalidKeyException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kpRSA.getPublic(), spec));
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kpEC.getPublic(), spec));
+
+ // mod_auth, wrong key type
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(),
+ spec.withAuthKey(kp2.getPublic())));
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.DECRYPT_MODE, kp.getPrivate(),
+ spec.withAuthKey(kp2.getPrivate())));
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(),
+ spec.withAuthKey(kpRSA.getPrivate())));
+ Asserts.assertThrows(InvalidAlgorithmParameterException.class,
+ () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(),
+ spec.withAuthKey(kpEC.getPrivate())));
+ }
+
+ static void checkEncryptDecrypt(KeyPair kp, HPKEParameterSpec ps,
+ HPKEParameterSpec pr) throws Exception {
+
+ var c1 = Cipher.getInstance("HPKE");
+ var c2 = Cipher.getInstance("HPKE");
+ var aad = "AAD".getBytes(StandardCharsets.UTF_8);
+
+ c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), ps);
+ Asserts.assertEquals(16, c1.getBlockSize());
+ Asserts.assertEquals(116, c1.getOutputSize(100));
+ c1.updateAAD(aad);
+ var ct = c1.doFinal(new byte[2]);
+
+ c2.init(Cipher.DECRYPT_MODE, kp.getPrivate(),
+ pr.withEncapsulation(c1.getIV()));
+ Asserts.assertEquals(16, c2.getBlockSize());
+ Asserts.assertEquals(84, c2.getOutputSize(100));
+ c2.updateAAD(aad);
+ Asserts.assertEqualsByteArray(c2.doFinal(ct), new byte[2]);
+ }
+}
diff --git a/test/jdk/com/sun/crypto/provider/Cipher/HPKE/Functions.java b/test/jdk/com/sun/crypto/provider/Cipher/HPKE/Functions.java
new file mode 100644
index 0000000000000..9ebf4ce5c0973
--- /dev/null
+++ b/test/jdk/com/sun/crypto/provider/Cipher/HPKE/Functions.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import jdk.test.lib.Asserts;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.HPKEParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.spec.ECGenParameterSpec;
+import java.util.List;
+
+import static javax.crypto.spec.HPKEParameterSpec.AEAD_AES_128_GCM;
+import static javax.crypto.spec.HPKEParameterSpec.AEAD_AES_256_GCM;
+import static javax.crypto.spec.HPKEParameterSpec.AEAD_CHACHA20_POLY1305;
+import static javax.crypto.spec.HPKEParameterSpec.KDF_HKDF_SHA256;
+import static javax.crypto.spec.HPKEParameterSpec.KDF_HKDF_SHA384;
+import static javax.crypto.spec.HPKEParameterSpec.KDF_HKDF_SHA512;
+import static javax.crypto.spec.HPKEParameterSpec.KEM_DHKEM_P_256_HKDF_SHA256;
+import static javax.crypto.spec.HPKEParameterSpec.KEM_DHKEM_P_384_HKDF_SHA384;
+import static javax.crypto.spec.HPKEParameterSpec.KEM_DHKEM_P_521_HKDF_SHA512;
+import static javax.crypto.spec.HPKEParameterSpec.KEM_DHKEM_X25519_HKDF_SHA256;
+import static javax.crypto.spec.HPKEParameterSpec.KEM_DHKEM_X448_HKDF_SHA512;
+
+/*
+ * @test
+ * @bug 8325448
+ * @library /test/lib
+ * @summary HPKE running with different keys
+ */
+public class Functions {
+
+ record Params(String name, int kem) {}
+ static List