mirror of
https://github.com/openjdk/jdk.git
synced 2026-01-28 12:09:14 +00:00
8325448: Hybrid Public Key Encryption
Reviewed-by: mullan, ascarpino, abarashev
This commit is contained in:
parent
b9ee9541cf
commit
45a2fd37f0
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
588
src/java.base/share/classes/com/sun/crypto/provider/HPKE.java
Normal file
588
src/java.base/share/classes/com/sun/crypto/provider/HPKE.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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) + "\")";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
289
test/jdk/com/sun/crypto/provider/Cipher/HPKE/Compliance.java
Normal file
289
test/jdk/com/sun/crypto/provider/Cipher/HPKE/Compliance.java
Normal 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]);
|
||||
}
|
||||
}
|
||||
113
test/jdk/com/sun/crypto/provider/Cipher/HPKE/Functions.java
Normal file
113
test/jdk/com/sun/crypto/provider/Cipher/HPKE/Functions.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
126
test/jdk/com/sun/crypto/provider/Cipher/HPKE/KAT9180.java
Normal file
126
test/jdk/com/sun/crypto/provider/Cipher/HPKE/KAT9180.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
153
test/jdk/sun/security/util/SliceableSecretKey/SoftSliceable.java
Normal file
153
test/jdk/sun/security/util/SliceableSecretKey/SoftSliceable.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user