8325448: Hybrid Public Key Encryption

Reviewed-by: mullan, ascarpino, abarashev
This commit is contained in:
Weijun Wang 2025-11-20 15:15:41 +00:00
parent b9ee9541cf
commit 45a2fd37f0
12 changed files with 2119 additions and 229 deletions

View File

@ -26,26 +26,51 @@ package com.sun.crypto.provider;
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 class DHKEM implements KEMSpi {
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 @@ public class DHKEM implements KEMSpi {
;
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 @@ public class DHKEM implements KEMSpi {
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 @@ public class DHKEM implements KEMSpi {
}
}
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 @@ public class DHKEM implements KEMSpi {
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 @@ public class DHKEM implements KEMSpi {
// 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 class DHKEM implements KEMSpi {
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 class DHKEM implements KEMSpi {
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));
}
private Params paramsFromKey(Key k) throws InvalidKeyException {
if (k instanceof ECKey eckey) {
if (ECUtil.equals(eckey.getParams(), CurveDB.P_256)) {
// 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(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 @@ public class DHKEM implements KEMSpi {
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 @@ public class DHKEM implements KEMSpi {
}
}
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);
}
}

View File

@ -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();
}
}

View File

@ -371,6 +371,8 @@ public final class SunJCE extends Provider {
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
*/

View File

@ -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
* <a href="https://www.rfc-editor.org/info/rfc9180">Hybrid Public Key Encryption</a>
* (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.
* <p>
* The <a href="{@docRoot}/../specs/security/standard-names.html#cipher-algorithms">
* standard algorithm name</a> 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".
* <p>
* 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}.
* <p>
* An {@code HPKEParameterSpec} object must be provided at HPKE
* {@linkplain Cipher#init(int, Key, AlgorithmParameterSpec) cipher initialization}.
* <p>
* 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 <a href="https://www.rfc-editor.org/rfc/rfc9180.html#section-7">Section 7</a>
* of RFC 9180 and maintained on the
* <a href="https://www.iana.org/assignments/hpke/hpke.xhtml">IANA HPKE page</a>.
* <p>
* Once an {@code HPKEParameterSpec} object is created, additional methods
* are available to generate new {@code HPKEParameterSpec} objects with
* different features:
* <ul>
* <li>
* Application-supplied information can be provided using the
* {@link #withInfo(byte[])} method by both sides.
* <li>
* To authenticate using a pre-shared key ({@code mode_psk}), the
* pre-shared key and its identifier must be provided using the
* {@link #withPsk(SecretKey, byte[])} method by both sides.
* <li>
* To authenticate using an asymmetric key ({@code mode_auth}),
* the asymmetric keys must be provided using the {@link #withAuthKey(AsymmetricKey)}
* method. Precisely, the sender must call this method with its own private key
* and the recipient must call it with the sender's public key.
* <li>
* To authenticate using both a PSK and an asymmetric key
* ({@code mode_auth_psk}), both {@link #withAuthKey(AsymmetricKey)} and
* {@link #withPsk(SecretKey, byte[])} methods must be called as described above.
* <li>
* In HPKE, a shared secret is negotiated during the KEM step and a key
* encapsulation message must be transmitted from the sender to the recipient
* so that the recipient can recover the shared secret. On the sender side,
* after the cipher is initialized, the key encapsulation message can be
* retrieved using the {@link Cipher#getIV()} method. On the recipient side,
* this message must be supplied as part of an {@code HPKEParameterSpec}
* object obtained from the {@link #withEncapsulation(byte[])} method.
* </ul>
* For successful interoperability, both sides need to have identical algorithm
* identifiers, and supply identical
* {@code info}, {@code psk}, and {@code psk_id} or matching authentication
* keys if provided. For details about HPKE modes, refer to
* <a href="https://www.rfc-editor.org/rfc/rfc9180.html#section-5">Section 5</a>
* of RFC 9180.
* <p>
* If an HPKE cipher is {@linkplain Cipher#init(int, Key) initialized without
* parameters}, an {@code InvalidKeyException} is thrown.
* <p>
* 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:
* <ul>
* <li> An algorithm identifier is unsupported or does not match the provided key type.
* <li> A key encapsulation message is provided on the sender side.
* <li> A key encapsulation message is not provided on the recipient side.
* <li> An attempt to use {@code withAuthKey(key)} is made with an incompatible key.
* <li> An attempt to use {@code withAuthKey(key)} is made but {@code mode_auth}
* or {@code mode_auth_psk} is not supported by the KEM algorithm used.
* </ul>
* After initialization, both the sender and recipient can process multiple
* messages in sequence with repeated {@code doFinal} calls, optionally preceded
* by one or more {@code updateAAD} and {@code update}. Each {@code doFinal}
* performs a complete HPKE encryption or decryption operation using a distinct
* IV derived from an internal sequence counter, as specified in
* <a href="https://www.rfc-editor.org/rfc/rfc9180.html#section-5.2">Section 5.2</a>
* of RFC 9180. On the recipient side, each {@code doFinal} call must correspond
* to exactly one complete ciphertext, and the number and order of calls must
* match those on the sender side. This differs from the direct use of an AEAD
* cipher, where the caller must provide a fresh IV and reinitialize the cipher
* for each message. By managing IVs internally, HPKE allows a single
* initialization to support multiple messages while still ensuring IV
* uniqueness and preserving AEAD security guarantees.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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) + "\")";
}
}
}

View File

@ -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
}
}

View File

@ -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 <code>SecretKey</code>s that support using its slice as a new
* <code>SecretKey</code>.
* <p>
* 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 <code>SecretKey</code>.
*
* @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 <code>from</code>
* and <code>to</code> values
* @throws UnsupportedOperationException if slicing is not supported
*/
SecretKey slice(String alg, int from, int to);
}

View File

@ -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]);
}
}

View File

@ -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<Params> PARAMS = List.of(
new Params("secp256r1", KEM_DHKEM_P_256_HKDF_SHA256),
new Params("secp384r1", KEM_DHKEM_P_384_HKDF_SHA384),
new Params("secp521r1", KEM_DHKEM_P_521_HKDF_SHA512),
new Params("X25519", KEM_DHKEM_X25519_HKDF_SHA256),
new Params("X448", KEM_DHKEM_X448_HKDF_SHA512)
);
public static void main(String[] args) throws Exception {
var msg = "hello".getBytes(StandardCharsets.UTF_8);
var msg2 = "goodbye".getBytes(StandardCharsets.UTF_8);
var info = "info".getBytes(StandardCharsets.UTF_8);
var psk = new SecretKeySpec("K".repeat(32).getBytes(StandardCharsets.UTF_8), "Generic");
var psk_id = "psk1".getBytes(StandardCharsets.UTF_8);
for (var param : PARAMS) {
var c1 = Cipher.getInstance("HPKE");
var c2 = Cipher.getInstance("HPKE");
var kp = genKeyPair(param.name());
var kp2 = genKeyPair(param.name());
for (var kdf : List.of(KDF_HKDF_SHA256, KDF_HKDF_SHA384, KDF_HKDF_SHA512)) {
for (var aead : List.of(AEAD_AES_256_GCM, AEAD_AES_128_GCM, AEAD_CHACHA20_POLY1305)) {
var params = HPKEParameterSpec.of(param.kem, kdf, aead);
System.out.println(params);
c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), params);
c2.init(Cipher.DECRYPT_MODE, kp.getPrivate(), params.withEncapsulation(c1.getIV()));
Asserts.assertEqualsByteArray(msg, c2.doFinal(c1.doFinal(msg)));
Asserts.assertEqualsByteArray(msg2, c2.doFinal(c1.doFinal(msg2)));
c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), params
.withAuthKey(kp2.getPrivate())
.withInfo(info)
.withPsk(psk, psk_id));
c2.init(Cipher.DECRYPT_MODE, kp.getPrivate(), params
.withAuthKey(kp2.getPublic())
.withInfo(info)
.withPsk(psk, psk_id)
.withEncapsulation(c1.getIV()));
Asserts.assertEqualsByteArray(msg, c2.doFinal(c1.doFinal(msg)));
Asserts.assertEqualsByteArray(msg2, c2.doFinal(c1.doFinal(msg2)));
}
}
}
}
static KeyPair genKeyPair(String name) throws Exception {
if (name.startsWith("secp")) {
var g = KeyPairGenerator.getInstance("EC");
g.initialize(new ECGenParameterSpec(name));
return g.generateKeyPair();
} else {
return KeyPairGenerator.getInstance(name).generateKeyPair();
}
}
}

View File

@ -0,0 +1,126 @@
/*
* 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.
*/
/*
* @test
* @bug 8325448
* @summary KAT inside RFC 9180
* @library /test/lib
* @modules java.base/com.sun.crypto.provider
*/
import jdk.test.lib.Asserts;
import jdk.test.lib.artifacts.Artifact;
import jdk.test.lib.artifacts.ArtifactResolver;
import jdk.test.lib.json.JSONValue;
import com.sun.crypto.provider.DHKEM;
import javax.crypto.Cipher;
import javax.crypto.spec.HPKEParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HexFormat;
/// This test is based on Appendix A (Test Vectors) of
/// [RFC 9180](https://datatracker.ietf.org/doc/html/rfc9180#name-test-vectors)
/// The test data is available as a JSON file at:
/// https://github.com/cfrg/draft-irtf-cfrg-hpke/blob/5f503c564da00b0687b3de75f1dfbdfc4079ad31/test-vectors.json.
///
/// The JSON file can either be hosted on an artifactory server or
/// provided via a local path with
/// ```
/// jtreg -Djdk.test.lib.artifacts.rfc9180-test-vectors=<local-json-file> KAT9180.java
/// ```
public class KAT9180 {
@Artifact(
organization = "jpg.tests.jdk.ietf",
name = "rfc9180-test-vectors",
revision = "5f503c5",
extension = "json",
unpack = false)
private static class RFC_9180_KAT {
}
public static void main(String[] args) throws Exception {
var h = HexFormat.of();
Path archivePath = ArtifactResolver.fetchOne(RFC_9180_KAT.class);
System.out.println("Data path: " + archivePath);
var c1 = Cipher.getInstance("HPKE");
var c2 = Cipher.getInstance("HPKE");
var ts = JSONValue.parse(new String(Files.readAllBytes(archivePath), StandardCharsets.UTF_8));
for (var tg : ts.asArray()) {
var mode = Integer.parseInt(tg.get("mode").asString());
System.err.print('I');
var kem_id = Integer.parseInt(tg.get("kem_id").asString());
var kdf_id = Integer.parseInt(tg.get("kdf_id").asString());
var aead_id = Integer.parseInt(tg.get("aead_id").asString());
var ikmR = h.parseHex(tg.get("ikmR").asString());
var ikmE = h.parseHex(tg.get("ikmE").asString());
var info = h.parseHex(tg.get("info").asString());
var kpR = new DHKEM.RFC9180DeriveKeyPairSR(ikmR).derive(kem_id);
var spec = HPKEParameterSpec.of(kem_id, kdf_id, aead_id).withInfo(info);
var rand = new DHKEM.RFC9180DeriveKeyPairSR(ikmE);
if (mode == 1 || mode == 3) {
spec = spec.withPsk(
new SecretKeySpec(h.parseHex(tg.get("psk").asString()), "Generic"),
h.parseHex(tg.get("psk_id").asString()));
}
if (mode == 0 || mode == 1) {
c1.init(Cipher.ENCRYPT_MODE, kpR.getPublic(), spec, rand);
c2.init(Cipher.DECRYPT_MODE, kpR.getPrivate(),
spec.withEncapsulation(c1.getIV()));
} else {
var ikmS = h.parseHex(tg.get("ikmS").asString());
var kpS = new DHKEM.RFC9180DeriveKeyPairSR(ikmS).derive(kem_id);
c1.init(Cipher.ENCRYPT_MODE, kpR.getPublic(),
spec.withAuthKey(kpS.getPrivate()), rand);
c2.init(Cipher.DECRYPT_MODE, kpR.getPrivate(),
spec.withEncapsulation(c1.getIV()).withAuthKey(kpS.getPublic()));
}
var enc = tg.get("encryptions");
if (enc != null) {
System.err.print('e');
var count = 0;
for (var p : enc.asArray()) {
var aad = h.parseHex(p.get("aad").asString());
var pt = h.parseHex(p.get("pt").asString());
var ct = h.parseHex(p.get("ct").asString());
c1.updateAAD(aad);
var ct1 = c1.doFinal(pt);
Asserts.assertEqualsByteArray(ct, ct1);
c2.updateAAD(aad);
var pt1 = c2.doFinal(ct);
Asserts.assertEqualsByteArray(pt, pt1);
count++;
}
System.err.print(count);
}
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2023, 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
@ -31,7 +31,6 @@
* @run main/othervm Compliance
*/
import jdk.test.lib.Asserts;
import jdk.test.lib.Utils;
import javax.crypto.DecapsulateException;
import javax.crypto.KEM;
@ -41,9 +40,7 @@ import java.security.*;
import java.security.interfaces.ECPublicKey;
import java.security.spec.*;
import java.util.Arrays;
import java.util.Objects;
import java.util.Random;
import java.util.function.Consumer;
import com.sun.crypto.provider.DHKEM;
@ -66,12 +63,10 @@ public class Compliance {
private static void conform() {
new KEM.Encapsulated(new SecretKeySpec(new byte[1], "X"), new byte[0], new byte[0]);
new KEM.Encapsulated(new SecretKeySpec(new byte[1], "X"), new byte[0], null);
Utils.runAndCheckException(
() -> new KEM.Encapsulated(null, new byte[0], null),
NullPointerException.class);
Utils.runAndCheckException(
() -> new KEM.Encapsulated(new SecretKeySpec(new byte[1], "X"), null, null),
NullPointerException.class);
Asserts.assertThrows(NullPointerException.class,
() -> new KEM.Encapsulated(null, new byte[0], null));
Asserts.assertThrows(NullPointerException.class,
() -> new KEM.Encapsulated(new SecretKeySpec(new byte[1], "X"), null, null));
}
// basic should and shouldn't behaviors
@ -86,37 +81,33 @@ public class Compliance {
KEM.getInstance("DHKEM", (String) null);
KEM.getInstance("DHKEM", (Provider) null);
KEM kem = KEM.getInstance("DHKEM");
Utils.runAndCheckException(
() -> KEM.getInstance("OLALA"),
NoSuchAlgorithmException.class);
Utils.runAndCheckException(
() -> KEM.getInstance("DHKEM", "NoWhere"),
NoSuchProviderException.class);
Utils.runAndCheckException(
() -> KEM.getInstance("DHKEM", "SunRsaSign"),
NoSuchAlgorithmException.class);
Asserts.assertThrows(NoSuchAlgorithmException.class,
() -> KEM.getInstance("OLALA"));
Asserts.assertThrows(NoSuchProviderException.class,
() -> KEM.getInstance("DHKEM", "NoWhere"));
Asserts.assertThrows(NoSuchAlgorithmException.class,
() -> KEM.getInstance("DHKEM", "SunRsaSign"));
Utils.runAndCheckException(
() -> kem.newEncapsulator(null),
InvalidKeyException.class);
Utils.runAndCheckException(
() -> kem.newDecapsulator(null),
InvalidKeyException.class);
Asserts.assertThrows(InvalidKeyException.class,
() -> kem.newEncapsulator(null));
Asserts.assertThrows(InvalidKeyException.class,
() -> kem.newDecapsulator(null));
// Still an EC key, rejected by implementation
Utils.runAndCheckException(
() -> kem.newEncapsulator(badECKey()),
ExChecker.of(InvalidKeyException.class).by(DHKEM.class));
checkThrownBy(Asserts.assertThrows(
InvalidKeyException.class,
() -> kem.newEncapsulator(badECKey())),
DHKEM.class.getName());
// Not an EC key at all, rejected by framework coz it's not
// listed in "SupportedKeyClasses" in SunJCE.java.
Utils.runAndCheckException(
() -> kem.newEncapsulator(kpRSA.getPublic()),
ExChecker.of(InvalidKeyException.class).by(KEM.class.getName() + "$DelayedKEM"));
checkThrownBy(Asserts.assertThrows(
InvalidKeyException.class,
() -> kem.newEncapsulator(kpRSA.getPublic())),
KEM.class.getName() + "$DelayedKEM");
Utils.runAndCheckException(
() -> kem.newDecapsulator(kpRSA.getPrivate()),
InvalidKeyException.class);
Asserts.assertThrows(InvalidKeyException.class,
() -> kem.newDecapsulator(kpRSA.getPrivate()));
kem.newEncapsulator(kpX.getPublic(), null);
kem.newEncapsulator(kpX.getPublic(), null, null);
@ -125,15 +116,12 @@ public class Compliance {
Asserts.assertEQ(enc1.key().getEncoded().length, e2.secretSize());
Asserts.assertEQ(enc1.key().getAlgorithm(), "AES");
Utils.runAndCheckException(
() -> e2.encapsulate(-1, 12, "AES"),
IndexOutOfBoundsException.class);
Utils.runAndCheckException(
() -> e2.encapsulate(0, e2.secretSize() + 1, "AES"),
IndexOutOfBoundsException.class);
Utils.runAndCheckException(
() -> e2.encapsulate(0, e2.secretSize(), null),
NullPointerException.class);
Asserts.assertThrows(IndexOutOfBoundsException.class,
() -> e2.encapsulate(-1, 12, "AES"));
Asserts.assertThrows(IndexOutOfBoundsException.class,
() -> e2.encapsulate(0, e2.secretSize() + 1, "AES"));
Asserts.assertThrows(NullPointerException.class,
() -> e2.encapsulate(0, e2.secretSize(), null));
KEM.Encapsulated enc = e2.encapsulate();
Asserts.assertEQ(enc.key().getEncoded().length, e2.secretSize());
@ -162,29 +150,23 @@ public class Compliance {
d.secretSize() - 16, d.secretSize(), "AES");
Asserts.assertEQ(encTail.key(), decTail);
Utils.runAndCheckException(
() -> d.decapsulate(null),
NullPointerException.class);
Utils.runAndCheckException(
() -> d.decapsulate(enc.encapsulation(), -1, 12, "AES"),
IndexOutOfBoundsException.class);
Utils.runAndCheckException(
() -> d.decapsulate(enc.encapsulation(), 0, d.secretSize() + 1, "AES"),
IndexOutOfBoundsException.class);
Utils.runAndCheckException(
() -> d.decapsulate(enc.encapsulation(), 0, d.secretSize(), null),
NullPointerException.class);
Asserts.assertThrows(NullPointerException.class,
() -> d.decapsulate(null));
Asserts.assertThrows(IndexOutOfBoundsException.class,
() -> d.decapsulate(enc.encapsulation(), -1, 12, "AES"));
Asserts.assertThrows(IndexOutOfBoundsException.class,
() -> d.decapsulate(enc.encapsulation(), 0, d.secretSize() + 1, "AES"));
Asserts.assertThrows(NullPointerException.class,
() -> d.decapsulate(enc.encapsulation(), 0, d.secretSize(), null));
KEM.Encapsulator e3 = kem.newEncapsulator(kpEC.getPublic());
KEM.Encapsulated enc2 = e3.encapsulate();
KEM.Decapsulator d3 = kem.newDecapsulator(kpX.getPrivate());
Utils.runAndCheckException(
() -> d3.decapsulate(enc2.encapsulation()),
DecapsulateException.class);
Asserts.assertThrows(DecapsulateException.class,
() -> d3.decapsulate(enc2.encapsulation()));
Utils.runAndCheckException(
() -> d3.decapsulate(new byte[100]),
DecapsulateException.class);
Asserts.assertThrows(DecapsulateException.class,
() -> d3.decapsulate(new byte[100]));
}
static class MySecureRandom extends SecureRandom {
@ -273,34 +255,8 @@ public class Compliance {
};
}
// Used by Utils.runAndCheckException. Checks for type and final thrower.
record ExChecker(Class<? extends Throwable> ex, String caller)
implements Consumer<Throwable> {
ExChecker {
Objects.requireNonNull(ex);
}
static ExChecker of(Class<? extends Throwable> ex) {
return new ExChecker(ex, null);
}
ExChecker by(String caller) {
return new ExChecker(ex(), caller);
}
ExChecker by(Class<?> caller) {
return new ExChecker(ex(), caller.getName());
}
@Override
public void accept(Throwable t) {
if (t == null) {
throw new AssertionError("no exception thrown");
} else if (!ex.isAssignableFrom(t.getClass())) {
throw new AssertionError("exception thrown is " + t.getClass());
} else if (caller == null) {
return;
} else if (t.getStackTrace()[0].getClassName().equals(caller)) {
return;
} else {
throw new AssertionError("thrown by " + t.getStackTrace()[0].getClassName());
}
}
// Ensures `t` is thrown by `caller`
static <T extends Throwable> void checkThrownBy(T t, String caller) {
Asserts.assertEquals(caller, t.getStackTrace()[0].getClassName());
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 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
@ -42,6 +42,7 @@ import javax.crypto.KeyGenerator;
import javax.crypto.spec.ChaCha20ParameterSpec;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.HPKEParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.crypto.spec.SecretKeySpec;
@ -96,6 +97,11 @@ public class Deterministic {
key = new SecretKeySpec("isthisakey".getBytes(StandardCharsets.UTF_8), "PBE");
// Some cipher requires salt to be 8 byte long
spec = new PBEParameterSpec("saltsalt".getBytes(StandardCharsets.UTF_8), 100);
} else if (alg.equals("HPKE")) {
key = KeyPairGenerator.getInstance("x25519").generateKeyPair().getPublic();
spec = HPKEParameterSpec.of(HPKEParameterSpec.KEM_DHKEM_X25519_HKDF_SHA256,
HPKEParameterSpec.KDF_HKDF_SHA256,
HPKEParameterSpec.AEAD_AES_256_GCM);
} else {
key = generateKey(alg.split("/")[0], s.getProvider());
if (!alg.contains("/") || alg.contains("/ECB/")) {
@ -239,6 +245,8 @@ public class Deterministic {
return g.generateKey();
} if (s.equals("RSA")) {
return generateKeyPair("RSA", 3).getPublic();
} if (s.equals("HPKE")) {
return generateKeyPair("EC", 3).getPublic();
} else {
var g = KeyGenerator.getInstance(s, p);
g.init(new SeededSecureRandom(SEED + 4));

View File

@ -0,0 +1,153 @@
/*
* 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 sun.security.util.SliceableSecretKey;
import javax.crypto.KDF;
import javax.crypto.KDFParameters;
import javax.crypto.KDFSpi;
import javax.crypto.KEM;
import javax.crypto.SecretKey;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
/*
* @test
* @bug 8325448
* @library /test/lib /test/jdk/security/unsignedjce
* @build java.base/javax.crypto.ProviderVerifier
* @modules java.base/sun.security.util
* @run main/othervm SoftSliceable
* @summary Showcase how Sliceable can be used in DHKEM
*/
public class SoftSliceable {
public static void main(String[] args) throws Exception {
// Put an HKDF-SHA256 impl that is preferred to the SunJCE one
Security.insertProviderAt(new ProviderImpl(), 1);
// Just plain KEM calls
var kp = KeyPairGenerator.getInstance("X25519").generateKeyPair();
var k = KEM.getInstance("DHKEM");
var e = k.newEncapsulator(kp.getPublic());
var d = k.newDecapsulator(kp.getPrivate());
var enc = e.encapsulate(3, 9, "Generic");
var k2 = d.decapsulate(enc.encapsulation(), 3, 9, "Generic");
var k2full = d.decapsulate(enc.encapsulation());
if (enc.key() instanceof KeyImpl ki1
&& k2 instanceof KeyImpl ki2
&& k2full instanceof KeyImpl ki2full) {
// So the keys do come from the new provider, and
// 1. It has the correct length
Asserts.assertEquals(6, ki1.bytes.length);
// 2. encaps and decaps result in same keys
Asserts.assertEqualsByteArray(ki1.bytes, ki2.bytes);
// 3. The key is the correct slice from the full shared secret
Asserts.assertEqualsByteArray(
Arrays.copyOfRange(ki2full.bytes, 3, 9), ki2.bytes);
} else {
throw new Exception("Unexpected key types");
}
}
// A trivial SliceableSecretKey that is non-extractable with getBytes()
public static class KeyImpl implements SecretKey, SliceableSecretKey {
private final byte[] bytes;
private final String algorithm;
public KeyImpl(byte[] bytes, String algorithm) {
this.bytes = bytes.clone();
this.algorithm = algorithm;
}
@Override
public String getAlgorithm() {
return algorithm;
}
@Override
public String getFormat() {
return null;
}
@Override
public byte[] getEncoded() {
return null;
}
@Override
public SecretKey slice(String alg, int from, int to) {
return new KeyImpl(Arrays.copyOfRange(bytes, from, to), algorithm);
}
}
// Our new provider
public static class ProviderImpl extends Provider {
public ProviderImpl() {
super("A", "A", "A");
put("KDF.HKDF-SHA256", KDFImpl.class.getName());
}
}
// Our new HKDF-SHA256 impl that always returns a KeyImpl object
public static class KDFImpl extends KDFSpi {
public KDFImpl(KDFParameters p)
throws InvalidAlgorithmParameterException {
super(p);
}
@Override
protected KDFParameters engineGetParameters() {
return null;
}
@Override
protected SecretKey engineDeriveKey(String alg, AlgorithmParameterSpec spec)
throws InvalidAlgorithmParameterException {
try {
var kdf = KDF.getInstance("HKDF-SHA256", "SunJCE");
var bytes = kdf.deriveData(spec);
return new KeyImpl(bytes, alg);
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new AssertionError("Cannot happen", e);
}
}
@Override
protected byte[] engineDeriveData(AlgorithmParameterSpec spec) {
throw new UnsupportedOperationException("Cannot derive data");
}
}
}