diff --git a/src/java.base/share/classes/com/sun/crypto/provider/DHKEM.java b/src/java.base/share/classes/com/sun/crypto/provider/DHKEM.java new file mode 100644 index 00000000000..01a6e38d2aa --- /dev/null +++ b/src/java.base/share/classes/com/sun/crypto/provider/DHKEM.java @@ -0,0 +1,395 @@ +/* + * Copyright (c) 2023, 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.jca.JCAUtil; +import sun.security.ssl.HKDF; +import sun.security.util.*; + +import javax.crypto.*; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.security.interfaces.*; +import java.security.spec.*; +import java.util.Arrays; +import java.util.Objects; + +// 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[] + {'K', 'E', 'M'}; + private static final byte[] EAE_PRK = new byte[] + {'e', 'a', 'e', '_', 'p', 'r', 'k'}; + private static final byte[] SHARED_SECRET = new byte[] + {'s', 'h', 'a', 'r', 'e', 'd', '_', 's', 'e', 'c', 'r', 'e', 't'}; + private static final byte[] DKP_PRK = new byte[] + {'d', 'k', 'p', '_', 'p', 'r', 'k'}; + private static final byte[] CANDIDATE = new byte[] + {'c', 'a', 'n', 'd', 'i', 'd', 'a', 't', 'e'}; + private static final byte[] SK = new byte[] + {'s', 'k'}; + private static final byte[] HPKE_V1 = new byte[] + {'H', 'P', 'K', 'E', '-', 'v', '1'}; + private static final byte[] EMPTY = new byte[0]; + + private record Handler(Params params, SecureRandom secureRandom, + PrivateKey skR, PublicKey pkR) + implements EncapsulatorSpi, DecapsulatorSpi { + + @Override + public KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm) { + 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); + try { + byte[] dh = params.DH(skE, pkR); + byte[] key = params.ExtractAndExpand(dh, kem_context); + return new KEM.Encapsulated( + new SecretKeySpec(key, from, to - from, algorithm), + pkEm, null); + } catch (Exception e) { + throw new ProviderException("internal error", e); + } + } + + @Override + public SecretKey engineDecapsulate(byte[] encapsulation, + int from, int to, String algorithm) throws DecapsulateException { + Objects.checkFromToIndex(from, to, params.Nsecret); + Objects.requireNonNull(algorithm, "null algorithm"); + Objects.requireNonNull(encapsulation, "null encapsulation"); + if (encapsulation.length != params.Npk) { + throw new DecapsulateException("incorrect encapsulation size"); + } + try { + PublicKey pkE = params.DeserializePublicKey(encapsulation); + byte[] dh = params.DH(skR, pkE); + byte[] pkRm = params.SerializePublicKey(pkR); + byte[] kem_context = concat(encapsulation, pkRm); + byte[] key = params.ExtractAndExpand(dh, kem_context); + return new SecretKeySpec(key, from, to - from, algorithm); + } catch (IOException | InvalidKeyException e) { + throw new DecapsulateException("Cannot decapsulate", e); + } catch (Exception e) { + throw new ProviderException("internal error", e); + } + } + + @Override + public int engineSecretSize() { + return params.Nsecret; + } + + @Override + public int engineEncapsulationSize() { + 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; + + private final byte[] ikm; + + public RFC9180DeriveKeyPairSR(byte[] ikm) { + super(null, null); // lightest constructor + this.ikm = ikm; + } + + public KeyPair derive(Params params) { + try { + return params.deriveKeyPair(ikm); + } catch (Exception e) { + throw new UnsupportedOperationException(e); + } + } + + public KeyPair derive(int kem_id) { + Params params = Arrays.stream(Params.values()) + .filter(p -> p.kem_id == kem_id) + .findFirst() + .orElseThrow(); + return derive(params); + } + } + + private enum Params { + + P256(0x10, 32, 32, 2 * 32 + 1, + "ECDH", "EC", CurveDB.P_256, "SHA-256"), + + P384(0x11, 48, 48, 2 * 48 + 1, + "ECDH", "EC", CurveDB.P_384, "SHA-384"), + + P521(0x12, 64, 66, 2 * 66 + 1, + "ECDH", "EC", CurveDB.P_521, "SHA-512"), + + X25519(0x20, 32, 32, 32, + "XDH", "XDH", NamedParameterSpec.X25519, "SHA-256"), + + X448(0x21, 64, 56, 56, + "XDH", "XDH", NamedParameterSpec.X448, "SHA-512"), + ; + + private final int kem_id; + private final int Nsecret; + private final int Nsk; + private final int Npk; + private final String kaAlgorithm; + private final String keyAlgorithm; + private final AlgorithmParameterSpec spec; + private final String hkdfAlgorithm; + + private final byte[] suiteId; + + 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.kaAlgorithm = kaAlgorithm; + this.keyAlgorithm = keyAlgorithm; + this.hkdfAlgorithm = hkdfAlgorithm; + suiteId = concat(KEM, I2OSP(kem_id, 2)); + } + + private boolean isEC() { + return this == P256 || this == P384 || this == P521; + } + + private KeyPair generateKeyPair(SecureRandom sr) { + if (sr instanceof RFC9180DeriveKeyPairSR r9) { + return r9.derive(this); + } + try { + KeyPairGenerator g = KeyPairGenerator.getInstance(keyAlgorithm); + g.initialize(spec, sr); + return g.generateKeyPair(); + } catch (Exception e) { + throw new ProviderException("internal error", e); + } + } + + 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); + } + } + + private PublicKey DeserializePublicKey(byte[] data) + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + KeySpec keySpec; + if (isEC()) { + NamedCurve curve = (NamedCurve) this.spec; + keySpec = new ECPublicKeySpec( + ECUtil.decodePoint(data, curve.getCurve()), curve); + } else { + data = data.clone(); + ArrayUtil.reverse(data); + keySpec = new XECPublicKeySpec( + this.spec, new BigInteger(1, data)); + } + return KeyFactory.getInstance(keyAlgorithm).generatePublic(keySpec); + } + + private byte[] DH(PrivateKey skE, PublicKey pkR) + throws NoSuchAlgorithmException, InvalidKeyException { + KeyAgreement ka = KeyAgreement.getInstance(kaAlgorithm); + ka.init(skE); + ka.doPhase(pkR, true); + return ka.generateSecret(); + } + + private byte[] ExtractAndExpand(byte[] dh, byte[] kem_context) + throws NoSuchAlgorithmException, InvalidKeyException { + HKDF kdf = new HKDF(hkdfAlgorithm); + SecretKey eae_prk = LabeledExtract(kdf, suiteId, null, EAE_PRK, dh); + return LabeledExpand(kdf, suiteId, eae_prk, SHARED_SECRET, + kem_context, Nsecret); + } + + private PublicKey getPublicKey(PrivateKey sk) + throws InvalidKeyException { + if (!(sk instanceof InternalPrivateKey)) { + try { + KeyFactory kf = KeyFactory.getInstance(keyAlgorithm, "SunEC"); + sk = (PrivateKey) kf.translateKey(sk); + } catch (Exception e) { + throw new InvalidKeyException("Error translating key", e); + } + } + if (sk instanceof InternalPrivateKey ik) { + try { + return ik.calculatePublicKey(); + } catch (UnsupportedOperationException e) { + throw new InvalidKeyException("Error retrieving key", e); + } + } else { + // Should not happen, unless SunEC goes wrong + throw new ProviderException("Unknown key"); + } + } + + // For KAT tests only. See RFC9180DeriveKeyPairSR. + public KeyPair deriveKeyPair(byte[] ikm) throws Exception { + HKDF kdf = new HKDF(hkdfAlgorithm); + SecretKey dkp_prk = LabeledExtract(kdf, suiteId, null, DKP_PRK, 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) { + throw new RuntimeException(); + } + byte[] bytes = LabeledExpand(kdf, 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; + } + PrivateKey k = DeserializePrivateKey(sk.toByteArray()); + return new KeyPair(getPublicKey(k), k); + } else { + byte[] sk = LabeledExpand(kdf, suiteId, dkp_prk, SK, EMPTY, Nsk); + PrivateKey k = DeserializePrivateKey(sk); + return new KeyPair(getPublicKey(k), k); + } + } + + private PrivateKey DeserializePrivateKey(byte[] data) throws Exception { + KeySpec keySpec = isEC() + ? new ECPrivateKeySpec(new BigInteger(1, (data)), (NamedCurve) spec) + : new XECPrivateKeySpec(spec, data); + return KeyFactory.getInstance(keyAlgorithm).generatePrivate(keySpec); + } + } + + private static SecureRandom getSecureRandom(SecureRandom userSR) { + return userSR != null ? userSR : JCAUtil.getSecureRandom(); + } + + @Override + public EncapsulatorSpi engineNewEncapsulator( + PublicKey pk, AlgorithmParameterSpec spec, SecureRandom secureRandom) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (pk == null) { + throw new InvalidKeyException("input key is null"); + } + if (spec != null) { + throw new InvalidAlgorithmParameterException("no spec needed"); + } + Params params = paramsFromKey(pk); + return new Handler(params, getSecureRandom(secureRandom), null, pk); + } + + @Override + public DecapsulatorSpi engineNewDecapsulator(PrivateKey sk, AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (sk == null) { + throw new InvalidKeyException("input key is null"); + } + if (spec != null) { + throw new InvalidAlgorithmParameterException("no spec needed"); + } + Params params = paramsFromKey(sk); + return new Handler(params, 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)) { + return Params.P256; + } else if (ECUtil.equals(eckey.getParams(), CurveDB.P_384)) { + return Params.P384; + } else if (ECUtil.equals(eckey.getParams(), CurveDB.P_521)) { + return Params.P521; + } + } else if (k instanceof XECKey xkey + && xkey.getParams() instanceof NamedParameterSpec ns) { + if (ns.getName().equals("X25519")) { + return Params.X25519; + } else if (ns.getName().equals("X448")) { + return Params.X448; + } + } + throw new InvalidKeyException("Unsupported key"); + } + + private static byte[] concat(byte[]... inputs) { + ByteArrayOutputStream o = new ByteArrayOutputStream(); + Arrays.stream(inputs).forEach(o::writeBytes); + return o.toByteArray(); + } + + private static byte[] I2OSP(int n, int w) { + assert n < 256; + assert w == 1 || w == 2; + if (w == 1) { + return new byte[] { (byte) n }; + } else { + return new byte[] { (byte) (n >> 8), (byte) n }; + } + } + + private static SecretKey LabeledExtract(HKDF kdf, byte[] suite_id, + byte[] salt, byte[] label, byte[] ikm) throws InvalidKeyException { + return kdf.extract(salt, + new SecretKeySpec(concat(HPKE_V1, suite_id, label, ikm), "IKM"), + "HKDF-PRK"); + } + + private static byte[] LabeledExpand(HKDF kdf, 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); + return kdf.expand(prk, labeled_info, L, "NONE").getEncoded(); + } +} diff --git a/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java b/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java index 622434cfbaf..10093137151 100644 --- a/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java +++ b/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2023, 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 @@ -74,6 +74,10 @@ import static sun.security.util.SecurityProviderConstants.*; * * - HMAC-MD5, HMAC-SHA1, HMAC with SHA2 family and SHA3 family of digests * + * - JCEKS KeyStore + * + * - DHKEM + * */ public final class SunJCE extends Provider { @@ -743,6 +747,15 @@ public final class SunJCE extends Provider { ps("KeyStore", "JCEKS", "com.sun.crypto.provider.JceKeyStore"); + /* + * KEMs + */ + attrs.clear(); + attrs.put("ImplementedIn", "Software"); + attrs.put("SupportedKeyClasses", "java.security.interfaces.ECKey" + + "|java.security.interfaces.XECKey"); + ps("KEM", "DHKEM", "com.sun.crypto.provider.DHKEM", null, attrs); + /* * SSL/TLS mechanisms * diff --git a/src/java.base/share/classes/java/security/Provider.java b/src/java.base/share/classes/java/security/Provider.java index fa8c0bd5912..7246285b349 100644 --- a/src/java.base/share/classes/java/security/Provider.java +++ b/src/java.base/share/classes/java/security/Provider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2023, 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 @@ -1599,6 +1599,7 @@ public abstract class Provider extends Properties { addEngine("KeyAgreement", true, null); addEngine("KeyGenerator", false, null); addEngine("SecretKeyFactory", false, null); + addEngine("KEM", true, null); // JSSE addEngine("KeyManagerFactory", false, null); addEngine("SSLContext", false, null); diff --git a/src/java.base/share/classes/javax/crypto/DecapsulateException.java b/src/java.base/share/classes/javax/crypto/DecapsulateException.java new file mode 100644 index 00000000000..89efa9b6e48 --- /dev/null +++ b/src/java.base/share/classes/javax/crypto/DecapsulateException.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023, 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; + +import java.security.GeneralSecurityException; + +/** + * An exception that is thrown by the + * {@link javax.crypto.KEM.Decapsulator#decapsulate} method to denote an + * error during decapsulation. + * + * @since 21 + */ +public class DecapsulateException extends GeneralSecurityException { + + @java.io.Serial + private static final long serialVersionUID = 21L; + + /** + * Creates a {@code DecapsulateException} with the specified + * detail message. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + */ + public DecapsulateException(String message) { + super(message); + } + + /** + * Creates a {@code DecapsulateException} with the specified + * detail message and cause. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is permitted, + * and indicates that the cause is nonexistent or unknown.) + */ + public DecapsulateException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/java.base/share/classes/javax/crypto/KEM.java b/src/java.base/share/classes/javax/crypto/KEM.java new file mode 100644 index 00000000000..e05027a7abc --- /dev/null +++ b/src/java.base/share/classes/javax/crypto/KEM.java @@ -0,0 +1,737 @@ +/* + * Copyright (c) 2023, 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; + +import sun.security.jca.GetInstance; + +import java.security.*; +import java.security.InvalidAlgorithmParameterException; +import java.security.spec.AlgorithmParameterSpec; +import java.util.List; +import java.util.Objects; + +/** + * This class provides the functionality of a Key Encapsulation Mechanism (KEM). + * A KEM can be used to secure symmetric keys using asymmetric or public key + * cryptography between two parties. The sender calls the encapsulate method + * to generate a secret key and a key encapsulation message, and the receiver + * calls the decapsulate method to recover the same secret key from + * the key encapsulation message. + *

+ * The {@code getInstance} method creates a new {@code KEM} object that + * implements the specified algorithm. + *

+ * A {@code KEM} object is immutable. It is safe to call multiple + * {@code newEncapsulator} and {@code newDecapsulator} methods on the + * same {@code KEM} object at the same time. + *

+ * If a provider is not specified in the {@code getInstance} method when + * instantiating a {@code KEM} object, the {@code newEncapsulator} and + * {@code newDecapsulator} methods may return encapsulators or decapsulators + * from different providers. The provider selected is based on the parameters + * passed to the {@code newEncapsulator} or {@code newDecapsulator} methods: + * the private or public key and the optional {@code AlgorithmParameterSpec}. + * The {@link Encapsulator#providerName} and {@link Decapsulator#providerName} + * methods return the name of the selected provider. + *

+ * {@code Encapsulator} and {@code Decapsulator} objects are also immutable. + * It is safe to invoke multiple {@code encapsulate} and {@code decapsulate} + * methods on the same {@code Encapsulator} or {@code Decapsulator} object + * at the same time. Each invocation of {@code encapsulate} will generate a + * new shared secret and key encapsulation message. + *

+ * + * Example: + * {@snippet lang = java: + * // Receiver side + * var kpg = KeyPairGenerator.getInstance("X25519"); + * var kp = kpg.generateKeyPair(); + * + * // Sender side + * var kem1 = KEM.getInstance("DHKEM"); + * var sender = kem1.newEncapsulator(kp.getPublic()); + * var encapsulated = sender.encapsulate(); + * var k1 = encapsulated.key(); + * + * // Receiver side + * var kem2 = KEM.getInstance("DHKEM"); + * var receiver = kem2.newDecapsulator(kp.getPrivate()); + * var k2 = receiver.decapsulate(encapsulated.encapsulation()); + * + * assert Arrays.equals(k1.getEncoded(), k2.getEncoded()); + * } + * + * @since 21 + */ +public final class KEM { + + /** + * This class specifies the return value of the encapsulate method of + * a Key Encapsulation Mechanism (KEM), which includes the shared secret + * (as a {@code SecretKey}), the key encapsulation message, + * and optional parameters. + *

+ * Note: the key encapsulation message can be also referred to as ciphertext. + * + * @see #newEncapsulator(PublicKey, AlgorithmParameterSpec, SecureRandom) + * @see Encapsulator#encapsulate(int, int, String) + * + * @since 21 + */ + public static final class Encapsulated { + private final SecretKey key; + private final byte[] encapsulation; + private final byte[] params; + + /** + * Constructs an {@code Encapsulated} object. + * + * @param key the shared secret as a key, must not be {@code null}. + * @param encapsulation the key encapsulation message, must not + * be {@code null}. The contents of the array are copied + * to protect against subsequent modification. + * @param params optional parameters, can be {@code null}. + * The contents of the array are copied to protect + * against subsequent modification. + * @throws NullPointerException if {@code key} or {@code encapsulation} + * is {@code null} + */ + public Encapsulated(SecretKey key, byte[] encapsulation, byte[] params) { + Objects.requireNonNull(key); + Objects.requireNonNull(encapsulation); + this.key = key; + this.encapsulation = encapsulation.clone(); + this.params = params == null ? null : params.clone(); + } + + /** + * Returns the {@code SecretKey}. + * + * @return the secret key + */ + public SecretKey key() { + return key; + } + + /** + * Returns the key encapsulation message. + * + * @return the key encapsulation message. A new copy of the byte array + * is returned. + */ + public byte[] encapsulation() { + return encapsulation.clone(); + } + + /** + * Returns the optional parameters in a byte array. + * + * @return the optional parameters in a byte array or {@code null} + * if not specified. A new copy of the byte array is returned. + */ + public byte[] params() { + return params == null ? null : params.clone(); + } + } + + /** + * An encapsulator, generated by {@link #newEncapsulator} on the KEM + * sender side. + *

+ * This class represents the key encapsulation function of a KEM. + * Each invocation of the {@code encapsulate} method generates a + * new secret key and key encapsulation message that is returned + * in an {@link Encapsulated} object. + * + * @since 21 + */ + public static final class Encapsulator { + + private final KEMSpi.EncapsulatorSpi e; + private final Provider p; + + private Encapsulator(KEMSpi.EncapsulatorSpi e, Provider p) { + assert e != null; + assert p != null; + this.e = e; + this.p = p; + } + + /** + * Returns the name of the provider. + * + * @return the name of the provider + */ + public String providerName() { + return p.getName(); + } + + /** + * The key encapsulation function. + *

+ * This method is equivalent to + * {@code encapsulate(0, secretSize(), "Generic")}. This combination + * of arguments must be supported by every implementation. + *

+ * The generated secret key is usually passed to a key derivation + * function (KDF) as the input keying material. + * + * @return a {@link Encapsulated} object containing the shared + * secret, key encapsulation message, and optional parameters. + * The shared secret is a {@code SecretKey} containing all of + * the bytes of the secret, and an algorithm name of "Generic". + */ + public Encapsulated encapsulate() { + return encapsulate(0, secretSize(), "Generic"); + } + + /** + * The key encapsulation function. + *

+ * Each invocation of this method generates a new secret key and key + * encapsulation message that is returned in an {@link Encapsulated} object. + *

+ * An implementation may choose to not support arbitrary combinations + * of {@code from}, {@code to}, and {@code algorithm}. + * + * @param from the initial index of the shared secret byte array + * to be returned, inclusive + * @param to the final index of the shared secret byte array + * to be returned, exclusive + * @param algorithm the algorithm name for the secret key that is returned + * @return a {@link Encapsulated} object containing a portion of + * the shared secret, key encapsulation message, and optional + * parameters. The portion of the shared secret is a + * {@code SecretKey} containing the bytes of the secret + * ranging from {@code from} to {@code to}, exclusive, + * and an algorithm name as specified. For example, + * {@code encapsulate(0, 16, "AES")} uses the first 16 bytes + * of the shared secret as a 128-bit AES key. + * @throws IndexOutOfBoundsException if {@code from < 0}, + * {@code from > to}, or {@code to > secretSize()} + * @throws NullPointerException if {@code algorithm} is {@code null} + * @throws UnsupportedOperationException if the combination of + * {@code from}, {@code to}, and {@code algorithm} + * is not supported by the encapsulator + */ + public Encapsulated encapsulate(int from, int to, String algorithm) { + return e.engineEncapsulate(from, to, algorithm); + } + + /** + * Returns the size of the shared secret. + *

+ * This method can be called to find out the length of the shared secret + * before {@code encapsulate} is called or if the obtained + * {@code SecretKey} is not extractable. + * + * @return the size of the shared secret + */ + public int secretSize() { + int result = e.engineSecretSize(); + assert result >= 0 && result != Integer.MAX_VALUE + : "invalid engineSecretSize result"; + return result; + } + + /** + * Returns the size of the key encapsulation message. + *

+ * This method can be called to find out the length of the encapsulation + * message before {@code encapsulate} is called. + * + * @return the size of the key encapsulation message + */ + public int encapsulationSize() { + int result = e.engineEncapsulationSize(); + assert result >= 0 && result != Integer.MAX_VALUE + : "invalid engineEncapsulationSize result"; + return result; + } + } + + /** + * A decapsulator, generated by {@link #newDecapsulator} on the KEM + * receiver side. + *

+ * This class represents the key decapsulation function of a KEM. + * An invocation of the {@code decapsulate} method recovers the + * secret key from the key encapsulation message. + * + * @since 21 + */ + public static final class Decapsulator { + private final KEMSpi.DecapsulatorSpi d; + private final Provider p; + + private Decapsulator(KEMSpi.DecapsulatorSpi d, Provider p) { + assert d != null; + assert p != null; + this.d = d; + this.p = p; + } + + /** + * Returns the name of the provider. + * + * @return the name of the provider + */ + public String providerName() { + return p.getName(); + } + + /** + * The key decapsulation function. + *

+ * This method is equivalent to + * {@code decapsulate(encapsulation, 0, secretSize(), "Generic")}. This + * combination of arguments must be supported by every implementation. + *

+ * The generated secret key is usually passed to a key derivation + * function (KDF) as the input keying material. + * + * @param encapsulation the key encapsulation message from the sender. + * The size must be equal to the value returned by + * {@link #encapsulationSize()}, or a {@code DecapsulateException} + * will be thrown. + * @return the shared secret as a {@code SecretKey} with + * an algorithm name of "Generic" + * @throws DecapsulateException if an error occurs during the + * decapsulation process + * @throws NullPointerException if {@code encapsulation} is {@code null} + */ + public SecretKey decapsulate(byte[] encapsulation) throws DecapsulateException { + return decapsulate(encapsulation, 0, secretSize(), "Generic"); + } + + /** + * The key decapsulation function. + *

+ * An invocation of this method recovers the secret key from the key + * encapsulation message. + *

+ * An implementation may choose to not support arbitrary combinations + * of {@code from}, {@code to}, and {@code algorithm}. + * + * @param encapsulation the key encapsulation message from the sender. + * The size must be equal to the value returned by + * {@link #encapsulationSize()}, or a {@code DecapsulateException} + * will be thrown. + * @param from the initial index of the shared secret byte array + * to be returned, inclusive + * @param to the final index of the shared secret byte array + * to be returned, exclusive + * @param algorithm the algorithm name for the secret key that is returned + * @return a portion of the shared secret as a {@code SecretKey} + * containing the bytes of the secret ranging from {@code from} + * to {@code to}, exclusive, and an algorithm name as specified. + * For example, {@code decapsulate(encapsulation, secretSize() + * - 16, secretSize(), "AES")} uses the last 16 bytes + * of the shared secret as a 128-bit AES key. + * @throws DecapsulateException if an error occurs during the + * decapsulation process + * @throws IndexOutOfBoundsException if {@code from < 0}, + * {@code from > to}, or {@code to > secretSize()} + * @throws NullPointerException if {@code encapsulation} or + * {@code algorithm} is {@code null} + * @throws UnsupportedOperationException if the combination of + * {@code from}, {@code to}, and {@code algorithm} + * is not supported by the decapsulator + */ + public SecretKey decapsulate(byte[] encapsulation, + int from, int to, String algorithm) + throws DecapsulateException { + return d.engineDecapsulate( + encapsulation, + from, to, + algorithm); + } + + /** + * Returns the size of the shared secret. + *

+ * This method can be called to find out the length of the shared secret + * before {@code decapsulate} is called or if the obtained + * {@code SecretKey} is not extractable. + * + * @return the size of the shared secret + */ + public int secretSize() { + int result = d.engineSecretSize(); + assert result >= 0 && result != Integer.MAX_VALUE + : "invalid engineSecretSize result"; + return result; + } + + /** + * Returns the size of the key encapsulation message. + *

+ * This method can be used to extract the encapsulation message + * from a longer byte array if no length information is provided + * by a higher level protocol. + * + * @return the size of the key encapsulation message + */ + public int encapsulationSize() { + int result = d.engineEncapsulationSize(); + assert result >= 0 && result != Integer.MAX_VALUE + : "invalid engineEncapsulationSize result"; + return result; + } + } + + private static final class DelayedKEM { + + private final Provider.Service[] list; // non empty array + + private DelayedKEM(Provider.Service[] list) { + this.list = list; + } + + private Encapsulator newEncapsulator(PublicKey publicKey, + AlgorithmParameterSpec spec, SecureRandom secureRandom) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (publicKey == null) { + throw new InvalidKeyException("input key is null"); + } + RuntimeException re = null; + InvalidAlgorithmParameterException iape = null; + InvalidKeyException ike = null; + NoSuchAlgorithmException nsae = null; + for (Provider.Service service : list) { + if (!service.supportsParameter(publicKey)) { + continue; + } + try { + KEMSpi spi = (KEMSpi) service.newInstance(null); + return new Encapsulator( + spi.engineNewEncapsulator(publicKey, spec, secureRandom), + service.getProvider()); + } catch (NoSuchAlgorithmException e) { + nsae = merge(nsae, e); + } catch (InvalidAlgorithmParameterException e) { + iape = merge(iape, e); + } catch (InvalidKeyException e) { + ike = merge(ike, e); + } catch (RuntimeException e) { + re = merge(re, e); + } + } + if (iape != null) throw iape; + if (ike != null) throw ike; + if (nsae != null) { + throw new InvalidKeyException("No installed provider found", nsae); + } + throw new InvalidKeyException("No installed provider supports this key: " + + publicKey.getClass().getName(), re); + } + + private static T merge(T e1, T e2) { + if (e1 == null) { + return e2; + } else { + e1.addSuppressed(e2); + return e1; + } + } + + private Decapsulator newDecapsulator(PrivateKey privateKey, AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (privateKey == null) { + throw new InvalidKeyException("input key is null"); + } + RuntimeException re = null; + InvalidAlgorithmParameterException iape = null; + InvalidKeyException ike = null; + NoSuchAlgorithmException nsae = null; + for (Provider.Service service : list) { + if (!service.supportsParameter(privateKey)) { + continue; + } + try { + KEMSpi spi = (KEMSpi) service.newInstance(null); + return new Decapsulator( + spi.engineNewDecapsulator(privateKey, spec), + service.getProvider()); + } catch (NoSuchAlgorithmException e) { + nsae = merge(nsae, e); + } catch (InvalidAlgorithmParameterException e) { + iape = merge(iape, e); + } catch (InvalidKeyException e) { + ike = merge(ike, e); + } catch (RuntimeException e) { + re = merge(re, e); + } + } + if (iape != null) throw iape; + if (ike != null) throw ike; + if (nsae != null) { + throw new InvalidKeyException("No installed provider found", nsae); + } + throw new InvalidKeyException("No installed provider supports this key: " + + privateKey.getClass().getName(), re); + } + } + + // If delayed provider selection is needed + private final DelayedKEM delayed; + + // otherwise + private final KEMSpi spi; + private final Provider provider; + + private final String algorithm; + + private KEM(String algorithm, KEMSpi spi, Provider provider) { + assert spi != null; + assert provider != null; + this.delayed = null; + this.spi = spi; + this.provider = provider; + this.algorithm = algorithm; + } + + private KEM(String algorithm, DelayedKEM delayed) { + assert delayed != null; + this.delayed = delayed; + this.spi = null; + this.provider = null; + this.algorithm = algorithm; + } + + /** + * Returns a {@code KEM} object that implements the specified algorithm. + * + * @param algorithm the name of the KEM algorithm. + * See the {@code KEM} section in the + * Java Security Standard Algorithm Names Specification + * for information about standard KEM algorithm names. + * @return the new {@code KEM} object + * @throws NoSuchAlgorithmException if no {@code Provider} supports a + * {@code KEM} implementation for the specified algorithm + * @throws NullPointerException if {@code algorithm} is {@code null} + */ + public static KEM getInstance(String algorithm) + throws NoSuchAlgorithmException { + List list = GetInstance.getServices( + "KEM", + Objects.requireNonNull(algorithm, "null algorithm name")); + if (list.isEmpty()) { + throw new NoSuchAlgorithmException(algorithm + " KEM not available"); + } + return new KEM(algorithm, new DelayedKEM(list.toArray(new Provider.Service[0]))); + } + + /** + * Returns a {@code KEM} object that implements the specified algorithm + * from the specified security provider. + * + * @param algorithm the name of the KEM algorithm. + * See the {@code KEM} section in the + * Java Security Standard Algorithm Names Specification + * for information about standard KEM algorithm names. + * @param provider the provider. If {@code null}, this method is equivalent + * to {@link #getInstance(String)}. + * @return the new {@code KEM} object + * @throws NoSuchAlgorithmException if a {@code provider} is specified and + * it does not support the specified KEM algorithm, + * or if {@code provider} is {@code null} and there is no provider + * that supports a KEM implementation of the specified algorithm + * @throws NullPointerException if {@code algorithm} is {@code null} + */ + public static KEM getInstance(String algorithm, Provider provider) + throws NoSuchAlgorithmException { + if (provider == null) { + return getInstance(algorithm); + } + GetInstance.Instance instance = GetInstance.getInstance( + "KEM", + KEMSpi.class, + Objects.requireNonNull(algorithm, "null algorithm name"), + provider); + return new KEM(algorithm, (KEMSpi) instance.impl, instance.provider); + } + + /** + * Returns a {@code KEM} object that implements the specified algorithm + * from the specified security provider. + * + * @param algorithm the name of the KEM algorithm. + * See the {@code KEM} section in the + * Java Security Standard Algorithm Names Specification + * for information about standard KEM algorithm names. + * @param provider the provider. If {@code null}, this method is equivalent + * to {@link #getInstance(String)}. + * @return the new {@code KEM} object + * @throws NoSuchAlgorithmException if a {@code provider} is specified and + * it does not support the specified KEM algorithm, + * or if {@code provider} is {@code null} and there is no provider + * that supports a KEM implementation of the specified algorithm + * @throws NoSuchProviderException if the specified provider is not + * registered in the security provider list + * @throws NullPointerException if {@code algorithm} is {@code null} + */ + public static KEM getInstance(String algorithm, String provider) + throws NoSuchAlgorithmException, NoSuchProviderException { + if (provider == null) { + return getInstance(algorithm); + } + GetInstance.Instance instance = GetInstance.getInstance( + "KEM", + KEMSpi.class, + Objects.requireNonNull(algorithm, "null algorithm name"), + provider); + return new KEM(algorithm, (KEMSpi) instance.impl, instance.provider); + } + + /** + * Creates a KEM encapsulator on the KEM sender side. + *

+ * This method is equivalent to {@code newEncapsulator(publicKey, null, null)}. + * + * @param publicKey the receiver's public key, must not be {@code null} + * @return the encapsulator for this key + * @throws InvalidKeyException if {@code publicKey} is {@code null} or invalid + * @throws UnsupportedOperationException if this method is not supported + * because an {@code AlgorithmParameterSpec} must be provided + */ + public Encapsulator newEncapsulator(PublicKey publicKey) + throws InvalidKeyException { + try { + return newEncapsulator(publicKey, null, null); + } catch (InvalidAlgorithmParameterException e) { + throw new UnsupportedOperationException( + "AlgorithmParameterSpec must be provided", e); + } + } + + /** + * Creates a KEM encapsulator on the KEM sender side. + *

+ * This method is equivalent to {@code newEncapsulator(publicKey, null, secureRandom)}. + * + * @param publicKey the receiver's public key, must not be {@code null} + * @param secureRandom the source of randomness for encapsulation. + * If {@code} null, a default one from the + * implementation will be used. + * @return the encapsulator for this key + * @throws InvalidKeyException if {@code publicKey} is {@code null} or invalid + * @throws UnsupportedOperationException if this method is not supported + * because an {@code AlgorithmParameterSpec} must be provided + */ + public Encapsulator newEncapsulator(PublicKey publicKey, SecureRandom secureRandom) + throws InvalidKeyException { + try { + return newEncapsulator(publicKey, null, secureRandom); + } catch (InvalidAlgorithmParameterException e) { + throw new UnsupportedOperationException( + "AlgorithmParameterSpec must be provided", e); + } + } + + /** + * Creates a KEM encapsulator on the KEM sender side. + *

+ * An algorithm can define an {@code AlgorithmParameterSpec} child class to + * provide extra information in this method. This is especially useful if + * the same key can be used to derive shared secrets in different ways. + * If any extra information inside this object needs to be transmitted along + * with the key encapsulation message so that the receiver is able to create + * a matching decapsulator, it will be included as a byte array in the + * {@link Encapsulated#params} field inside the encapsulation output. + * In this case, the security provider should provide an + * {@code AlgorithmParameters} implementation using the same algorithm name + * as the KEM. The receiver can initiate such an {@code AlgorithmParameters} + * instance with the {@code params} byte array received and recover + * an {@code AlgorithmParameterSpec} object to be used in its + * {@link #newDecapsulator(PrivateKey, AlgorithmParameterSpec)} call. + * + * @param publicKey the receiver's public key, must not be {@code null} + * @param spec the optional parameter, can be {@code null} + * @param secureRandom the source of randomness for encapsulation. + * If {@code} null, a default one from the + * implementation will be used. + * @return the encapsulator for this key + * @throws InvalidAlgorithmParameterException if {@code spec} is invalid + * or one is required but {@code spec} is {@code null} + * @throws InvalidKeyException if {@code publicKey} is {@code null} or invalid + */ + public Encapsulator newEncapsulator(PublicKey publicKey, + AlgorithmParameterSpec spec, SecureRandom secureRandom) + throws InvalidAlgorithmParameterException, InvalidKeyException { + return delayed != null + ? delayed.newEncapsulator(publicKey, spec, secureRandom) + : new Encapsulator(spi.engineNewEncapsulator(publicKey, spec, secureRandom), provider); + } + + /** + * Creates a KEM decapsulator on the KEM receiver side. + *

+ * This method is equivalent to {@code newDecapsulator(privateKey, null)}. + * + * @param privateKey the receiver's private key, must not be {@code null} + * @return the decapsulator for this key + * @throws InvalidKeyException if {@code privateKey} is {@code null} or invalid + * @throws UnsupportedOperationException if this method is not supported + * because an {@code AlgorithmParameterSpec} must be provided + */ + public Decapsulator newDecapsulator(PrivateKey privateKey) + throws InvalidKeyException { + try { + return newDecapsulator(privateKey, null); + } catch (InvalidAlgorithmParameterException e) { + throw new UnsupportedOperationException(e); + } + } + + /** + * Creates a KEM decapsulator on the KEM receiver side. + * + * @param privateKey the receiver's private key, must not be {@code null} + * @param spec the parameter, can be {@code null} + * @return the decapsulator for this key + * @throws InvalidAlgorithmParameterException if {@code spec} is invalid + * or one is required but {@code spec} is {@code null} + * @throws InvalidKeyException if {@code privateKey} is {@code null} or invalid + */ + public Decapsulator newDecapsulator(PrivateKey privateKey, AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException, InvalidKeyException { + return delayed != null + ? delayed.newDecapsulator(privateKey, spec) + : new Decapsulator(spi.engineNewDecapsulator(privateKey, spec), provider); + } + + /** + * Returns the name of the algorithm for this {@code KEM} object. + * + * @return the name of the algorithm for this {@code KEM} object. + */ + public String getAlgorithm() { + return this.algorithm; + } +} diff --git a/src/java.base/share/classes/javax/crypto/KEMSpi.java b/src/java.base/share/classes/javax/crypto/KEMSpi.java new file mode 100644 index 00000000000..61d13aeb024 --- /dev/null +++ b/src/java.base/share/classes/javax/crypto/KEMSpi.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2023, 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; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; + +/** + * This class defines the Service Provider Interface (SPI) for the {@link KEM} + * class. A security provider implements this interface to provide an + * implementation of a Key Encapsulation Mechanism (KEM) algorithm. + *

+ * A KEM algorithm may support a family of configurations. Each configuration + * may accept different types of keys, cryptographic primitives, and sizes of + * shared secrets and key encapsulation messages. A configuration is defined + * by the KEM algorithm name, the key it uses, and an optional + * {@code AlgorithmParameterSpec} argument that is specified when creating + * an encapsulator or decapsulator. The result of calling + * {@link #engineNewEncapsulator} or {@link #engineNewDecapsulator} must return + * an encapsulator or decapsulator that maps to a single configuration, + * where its {@code engineSecretSize()} and {@code engineEncapsulationSize()} + * methods return constant values. + *

+ * A {@code KEMSpi} implementation must be immutable. It must be safe to + * call multiple {@code engineNewEncapsulator} and {@code engineNewDecapsulator} + * methods at the same time. + *

+ * {@code EncapsulatorSpi} and {@code DecapsulatorSpi} implementations must also + * be immutable. It must be safe to invoke multiple {@code encapsulate} and + * {@code decapsulate} methods at the same time. Each invocation of + * {@code encapsulate} should generate a new shared secret and key + * encapsulation message. + *

+ * For example, + * {@snippet lang = java: + * public static class MyKEMImpl implements KEMSpi { + * + * @Override + * public KEMSpi.EncapsulatorSpi engineNewEncapsulator(PublicKey publicKey, + * AlgorithmParameterSpec spec, SecureRandom secureRandom) + * throws InvalidAlgorithmParameterException, InvalidKeyException { + * if (!checkPublicKey(publicKey)) { + * throw new InvalidKeyException("unsupported key"); + * } + * if (!checkParameters(spec)) { + * throw new InvalidAlgorithmParameterException("unsupported params"); + * } + * return new MyEncapsulator(publicKey, spec, secureRandom); + * } + * + * class MyEncapsulator implements KEMSpi.EncapsulatorSpi { + * MyEncapsulator(PublicKey publicKey, AlgorithmParameterSpec spec, + * SecureRandom secureRandom){ + * this.spec = spec != null ? spec : getDefaultParameters(); + * this.secureRandom = secureRandom != null + * ? secureRandom + * : getDefaultSecureRandom(); + * this.publicKey = publicKey; + * } + * + * @Override + * public KEM.Encapsulated encapsulate(int from, int to, String algorithm) { + * byte[] encapsulation; + * byte[] secret; + * // calculating... + * return new KEM.Encapsulated( + * new SecretKeySpec(secret, from, to - from, algorithm), + * encapsulation, null); + * } + * + * // ... + * } + * + * // ... + * } + * } + * + * @see KEM + * @since 21 + */ +public interface KEMSpi { + + /** + * The KEM encapsulator implementation, generated by + * {@link #engineNewEncapsulator} on the KEM sender side. + * + * @see KEM.Encapsulator + * + * @since 21 + */ + interface EncapsulatorSpi { + /** + * The key encapsulation function. + *

+ * Each invocation of this method must generate a new secret key and key + * encapsulation message that is returned in an {@link KEM.Encapsulated} object. + *

+ * An implementation must support the case where {@code from} is 0, + * {@code to} is the same as the return value of {@code secretSize()}, + * and {@code algorithm} is "Generic". + * + * @param from the initial index of the shared secret byte array + * to be returned, inclusive + * @param to the final index of the shared secret byte array + * to be returned, exclusive + * @param algorithm the algorithm name for the secret key that is returned + * @return an {@link KEM.Encapsulated} object containing a portion of + * the shared secret as a key with the specified algorithm, + * key encapsulation message, and optional parameters. + * @throws IndexOutOfBoundsException if {@code from < 0}, + * {@code from > to}, or {@code to > secretSize()} + * @throws NullPointerException if {@code algorithm} is {@code null} + * @throws UnsupportedOperationException if the combination of + * {@code from}, {@code to}, and {@code algorithm} + * is not supported by the encapsulator + * @see KEM.Encapsulated + * @see KEM.Encapsulator#encapsulate(int, int, String) + */ + KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm); + + /** + * Returns the size of the shared secret. + * + * @return the size of the shared secret as a finite non-negative integer + * @see KEM.Encapsulator#secretSize() + */ + int engineSecretSize(); + + /** + * Returns the size of the key encapsulation message. + * + * @return the size of the key encapsulation message as a finite non-negative integer + * @see KEM.Encapsulator#encapsulationSize() + */ + int engineEncapsulationSize(); + } + + /** + * The KEM decapsulator implementation, generated by + * {@link #engineNewDecapsulator} on the KEM receiver side. + * + * @see KEM.Decapsulator + * + * @since 21 + */ + interface DecapsulatorSpi { + /** + * The key decapsulation function. + *

+ * An invocation of this method recovers the secret key from the key + * encapsulation message. + *

+ * An implementation must support the case where {@code from} is 0, + * {@code to} is the same as the return value of {@code secretSize()}, + * and {@code algorithm} is "Generic". + * + * @param encapsulation the key encapsulation message from the sender. + * The size must be equal to the value returned by + * {@link #engineEncapsulationSize()} ()}, or a + * {@code DecapsulateException} must be thrown. + * @param from the initial index of the shared secret byte array + * to be returned, inclusive + * @param to the final index of the shared secret byte array + * to be returned, exclusive + * @param algorithm the algorithm name for the secret key that is returned + * @return a portion of the shared secret as a {@code SecretKey} with + * the specified algorithm + * @throws DecapsulateException if an error occurs during the + * decapsulation process + * @throws IndexOutOfBoundsException if {@code from < 0}, + * {@code from > to}, or {@code to > secretSize()} + * @throws NullPointerException if {@code encapsulation} or + * {@code algorithm} is {@code null} + * @throws UnsupportedOperationException if the combination of + * {@code from}, {@code to}, and {@code algorithm} + * is not supported by the decapsulator + * @see KEM.Decapsulator#decapsulate(byte[], int, int, String) + */ + SecretKey engineDecapsulate(byte[] encapsulation, int from, int to, String algorithm) + throws DecapsulateException; + + /** + * Returns the size of the shared secret. + * + * @return the size of the shared secret as a finite non-negative integer + * @see KEM.Decapsulator#secretSize() + */ + int engineSecretSize(); + + /** + * Returns the size of the key encapsulation message. + * + * @return the size of the key encapsulation message as a finite non-negative integer + * @see KEM.Decapsulator#encapsulationSize() + */ + int engineEncapsulationSize(); + } + + /** + * Creates a KEM encapsulator on the KEM sender side. + * + * @param publicKey the receiver's public key, must not be {@code null} + * @param spec the optional parameter, can be {@code null} + * @param secureRandom the source of randomness for encapsulation. + * If {@code null}, the implementation must provide + * a default one. + * @return the encapsulator for this key + * @throws InvalidAlgorithmParameterException if {@code spec} is invalid + * or one is required but {@code spec} is {@code null} + * @throws InvalidKeyException if {@code publicKey} is {@code null} or invalid + * @see KEM#newEncapsulator(PublicKey, AlgorithmParameterSpec, SecureRandom) + */ + EncapsulatorSpi engineNewEncapsulator(PublicKey publicKey, + AlgorithmParameterSpec spec, SecureRandom secureRandom) + throws InvalidAlgorithmParameterException, InvalidKeyException; + + /** + * Creates a KEM decapsulator on the KEM receiver side. + * + * @param privateKey the receiver's private key, must not be {@code null} + * @param spec the optional parameter, can be {@code null} + * @return the decapsulator for this key + * @throws InvalidAlgorithmParameterException if {@code spec} is invalid + * or one is required but {@code spec} is {@code null} + * @throws InvalidKeyException if {@code privateKey} is {@code null} or invalid + * @see KEM#newDecapsulator(PrivateKey, AlgorithmParameterSpec) + */ + DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey, AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException, InvalidKeyException; +} diff --git a/src/java.base/share/classes/sun/security/ssl/HKDF.java b/src/java.base/share/classes/sun/security/ssl/HKDF.java index 1ea46807d75..5e5cd144305 100644 --- a/src/java.base/share/classes/sun/security/ssl/HKDF.java +++ b/src/java.base/share/classes/sun/security/ssl/HKDF.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2023, 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 @@ -43,7 +43,7 @@ import java.util.Objects; * digest algorithm will be used by the HMAC function as part of the HKDF * derivation process. */ -final class HKDF { +public final class HKDF { private final Mac hmacObj; private final int hmacLen; @@ -57,7 +57,7 @@ final class HKDF { * @throws NoSuchAlgorithmException if that message digest algorithm does * not have an HMAC variant supported on any available provider. */ - HKDF(String hashAlg) throws NoSuchAlgorithmException { + public HKDF(String hashAlg) throws NoSuchAlgorithmException { Objects.requireNonNull(hashAlg, "Must provide underlying HKDF Digest algorithm."); String hmacAlg = "Hmac" + hashAlg.replace("-", ""); @@ -82,7 +82,7 @@ final class HKDF { * @throws InvalidKeyException if the {@code salt} parameter cannot be * used to initialize the underlying HMAC. */ - SecretKey extract(SecretKey salt, SecretKey inputKey, String keyAlg) + public SecretKey extract(SecretKey salt, SecretKey inputKey, String keyAlg) throws InvalidKeyException { if (salt == null) { salt = new SecretKeySpec(new byte[hmacLen], "HKDF-Salt"); @@ -110,7 +110,7 @@ final class HKDF { * @throws InvalidKeyException if the {@code salt} parameter cannot be * used to initialize the underlying HMAC. */ - SecretKey extract(byte[] salt, SecretKey inputKey, String keyAlg) + public SecretKey extract(byte[] salt, SecretKey inputKey, String keyAlg) throws InvalidKeyException { if (salt == null) { salt = new byte[hmacLen]; @@ -133,7 +133,7 @@ final class HKDF { * @throws InvalidKeyException if the underlying HMAC operation cannot * be initialized using the provided {@code pseudoRandKey} object. */ - SecretKey expand(SecretKey pseudoRandKey, byte[] info, int outLen, + public SecretKey expand(SecretKey pseudoRandKey, byte[] info, int outLen, String keyAlg) throws InvalidKeyException { byte[] kdfOutput; diff --git a/src/java.base/share/classes/sun/security/util/CurveDB.java b/src/java.base/share/classes/sun/security/util/CurveDB.java index 37578c157bd..b3b44ff0684 100644 --- a/src/java.base/share/classes/sun/security/util/CurveDB.java +++ b/src/java.base/share/classes/sun/security/util/CurveDB.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2023, 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 @@ -39,6 +39,11 @@ import java.util.*; * @author Andreas Sterbenz */ public class CurveDB { + + public static final NamedCurve P_256; + public static final NamedCurve P_384; + public static final NamedCurve P_521; + private static final int P = 1; // prime curve private static final int B = 2; // binary curve private static final int PD = 5; // prime curve, mark as default @@ -109,7 +114,7 @@ public class CurveDB { return new BigInteger(s, 16); } - private static void add(KnownOIDs o, int type, String sfield, + private static NamedCurve add(KnownOIDs o, int type, String sfield, String a, String b, String x, String y, String n, int h) { BigInteger p = bi(sfield); ECField field; @@ -143,6 +148,8 @@ public class CurveDB { // the curve is marked as a default curve. lengthMap.put(len, params); } + + return params; } static { @@ -255,7 +262,7 @@ public class CurveDB { "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 1); - add(KnownOIDs.secp256r1, PD, + P_256 = add(KnownOIDs.secp256r1, PD, "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", "5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", @@ -264,7 +271,7 @@ public class CurveDB { "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 1); - add(KnownOIDs.secp384r1, PD, + P_384 = add(KnownOIDs.secp384r1, PD, "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF", "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC", "B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF", @@ -273,7 +280,7 @@ public class CurveDB { "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973", 1); - add(KnownOIDs.secp521r1, PD, + P_521 = add(KnownOIDs.secp521r1, PD, "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC", "0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00", diff --git a/src/jdk.crypto.ec/share/classes/sun/security/ec/ECOperations.java b/src/jdk.crypto.ec/share/classes/sun/security/ec/ECOperations.java index c750e510fc7..d4959aed463 100644 --- a/src/jdk.crypto.ec/share/classes/sun/security/ec/ECOperations.java +++ b/src/jdk.crypto.ec/share/classes/sun/security/ec/ECOperations.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2023, 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 @@ -541,7 +541,7 @@ public class ECOperations { final class Secp256R1GeneratorMultiplier implements PointMultiplier { private static final ECPoint generator = - CurveDB.lookup("secp256r1").getGenerator(); + CurveDB.P_256.getGenerator(); private static final PointMultiplier multiplier = new Secp256R1GeneratorMultiplier(); diff --git a/src/jdk.crypto.ec/share/classes/sun/security/ec/SunEC.java b/src/jdk.crypto.ec/share/classes/sun/security/ec/SunEC.java index 3cfb74c8115..7f8c4dba002 100644 --- a/src/jdk.crypto.ec/share/classes/sun/security/ec/SunEC.java +++ b/src/jdk.crypto.ec/share/classes/sun/security/ec/SunEC.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2023, 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 @@ -212,11 +212,8 @@ public final class SunEC extends Provider { boolean firstCurve = true; StringBuilder names = new StringBuilder(); - for (NamedCurve namedCurve : - List.of( - CurveDB.lookup("secp256r1"), - CurveDB.lookup("secp384r1"), - CurveDB.lookup("secp521r1"))) { + for (NamedCurve namedCurve : List.of( + CurveDB.P_256, CurveDB.P_384, CurveDB.P_521)) { if (!firstCurve) { names.append("|"); } else { diff --git a/test/jdk/com/sun/crypto/provider/DHKEM/Compliance.java b/test/jdk/com/sun/crypto/provider/DHKEM/Compliance.java new file mode 100644 index 00000000000..27b72b5cf7c --- /dev/null +++ b/test/jdk/com/sun/crypto/provider/DHKEM/Compliance.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2023, 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 8297878 + * @summary Key Encapsulation Mechanism API + * @library /test/lib + * @modules java.base/com.sun.crypto.provider + */ +import jdk.test.lib.Asserts; +import jdk.test.lib.Utils; + +import javax.crypto.DecapsulateException; +import javax.crypto.KEM; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +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; + +public class Compliance { + + public static void main(String[] args) throws Exception { + basic(); + conform(); + determined(); + try { + Security.insertProviderAt(new ProviderImpl(), 1); + delayed(); + } finally { + Security.removeProvider("XP"); + } + } + + // Encapsulated conformance checks + 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); + } + + // basic should and shouldn't behaviors + static void basic() throws Exception { + KeyPair kpRSA = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + KeyPair kpX = KeyPairGenerator.getInstance("X25519").generateKeyPair(); + + KeyPairGenerator ecg = KeyPairGenerator.getInstance("EC"); + ecg.initialize(new ECGenParameterSpec("secp256r1")); + KeyPair kpEC = ecg.generateKeyPair(); + + 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); + + Utils.runAndCheckException( + () -> kem.newEncapsulator(null), + InvalidKeyException.class); + Utils.runAndCheckException( + () -> kem.newDecapsulator(null), + InvalidKeyException.class); + + // Still an EC key, rejected by implementation + Utils.runAndCheckException( + () -> kem.newEncapsulator(badECKey()), + ExChecker.of(InvalidKeyException.class).by(DHKEM.class)); + + // 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")); + + Utils.runAndCheckException( + () -> kem.newDecapsulator(kpRSA.getPrivate()), + InvalidKeyException.class); + + kem.newEncapsulator(kpX.getPublic(), null); + kem.newEncapsulator(kpX.getPublic(), null, null); + KEM.Encapsulator e2 = kem.newEncapsulator(kpX.getPublic()); + KEM.Encapsulated enc1 = e2.encapsulate(0, e2.secretSize(), "AES"); + 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); + + KEM.Encapsulated enc = e2.encapsulate(); + Asserts.assertEQ(enc.key().getEncoded().length, e2.secretSize()); + Asserts.assertEQ(enc.key().getAlgorithm(), "Generic"); + + kem.newDecapsulator(kpX.getPrivate(), null); + KEM.Decapsulator d = kem.newDecapsulator(kpX.getPrivate()); + d.decapsulate(enc.encapsulation()); + SecretKey dec = d.decapsulate(enc.encapsulation()); + Asserts.assertTrue(Arrays.equals(enc.key().getEncoded(), dec.getEncoded())); + + dec = d.decapsulate(enc.encapsulation(), 0, d.secretSize(), "AES"); + Asserts.assertTrue(Arrays.equals(enc.key().getEncoded(), dec.getEncoded())); + + KEM.Encapsulated encHead = e2.encapsulate(0, 16, "AES"); + Asserts.assertEQ(encHead.key().getEncoded().length, 16); + Asserts.assertEQ(encHead.key().getAlgorithm(), "AES"); + SecretKey decHead = d.decapsulate(encHead.encapsulation(), 0, 16, "AES"); + Asserts.assertEQ(encHead.key(), decHead); + + KEM.Encapsulated encTail = e2.encapsulate( + e2.secretSize() - 16, e2.secretSize(), "AES"); + Asserts.assertEQ(encTail.key().getEncoded().length, 16); + Asserts.assertEQ(encTail.key().getAlgorithm(), "AES"); + SecretKey decTail = d.decapsulate(encTail.encapsulation(), + 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); + + 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); + + Utils.runAndCheckException( + () -> d3.decapsulate(new byte[100]), + DecapsulateException.class); + } + + static class MySecureRandom extends SecureRandom { + final Random ran; + + MySecureRandom(long seed) { + ran = new Random(seed); + } + + @Override + public void nextBytes(byte[] bytes) { + ran.nextBytes(bytes); + } + } + + // Same random should generate same key encapsulation messages + static void determined() throws Exception { + long seed = new Random().nextLong(); + byte[] enc1 = calcDetermined(seed); + byte[] enc2 = calcDetermined(seed); + Asserts.assertTrue(Arrays.equals(enc1, enc2), + "Undetermined for " + seed); + } + + static byte[] calcDetermined(long seed) throws Exception { + SecureRandom random = new MySecureRandom(seed); + KeyPairGenerator g = KeyPairGenerator.getInstance("XDH"); + g.initialize(NamedParameterSpec.X25519, random); + PublicKey pk = g.generateKeyPair().getPublic(); + KEM kem = KEM.getInstance("DHKEM"); + kem.newEncapsulator(pk, random); // skip one + KEM.Encapsulator e = kem.newEncapsulator(pk, random); + byte[] enc1 = e.encapsulate().encapsulation(); + byte[] enc2 = e.encapsulate().encapsulation(); + Asserts.assertFalse(Arrays.equals(enc1, enc2)); + return enc2; + } + + public static class ProviderImpl extends Provider { + ProviderImpl() { + super("XP", "1", "XP"); + put("KEM.DHKEM", "Compliance$KEMImpl"); + } + } + + static boolean isEven(Key k) { + return Arrays.hashCode(k.getEncoded()) % 2 == 0; + } + + public static class KEMImpl extends DHKEM { + + @Override + public EncapsulatorSpi engineNewEncapsulator(PublicKey pk, AlgorithmParameterSpec spec, SecureRandom secureRandom) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (!isEven(pk)) throw new InvalidKeyException("Only accept even keys"); + return super.engineNewEncapsulator(pk, spec, secureRandom); + } + + @Override + public DecapsulatorSpi engineNewDecapsulator(PrivateKey sk, AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (!isEven(sk)) throw new InvalidKeyException("Only accept even keys"); + return super.engineNewDecapsulator(sk, spec); + } + } + + // Ensure delayed provider selection + static void delayed() throws Exception { + KeyPairGenerator g = KeyPairGenerator.getInstance("X25519"); + PublicKey even = null, odd = null; + while (even == null || odd == null) { + KeyPair kp = g.generateKeyPair(); + if (isEven(kp.getPublic())) { + even = kp.getPublic(); + } + if (!isEven(kp.getPublic())) { + odd = kp.getPublic(); + } + } + KEM kem = KEM.getInstance("DHKEM"); + + KEM.Encapsulator eodd = kem.newEncapsulator(odd); + KEM.Encapsulator eeven = kem.newEncapsulator(even); + Asserts.assertEQ(eodd.providerName(), "SunJCE"); + Asserts.assertEQ(eeven.providerName(), "XP"); + } + + static ECPublicKey badECKey() { + return new ECPublicKey() { + @Override + public ECPoint getW() { + return null; + } + + @Override + public String getAlgorithm() { + return null; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public byte[] getEncoded() { + return new byte[0]; + } + + @Override + public ECParameterSpec getParams() { + return null; + } + }; + } + + // Used by Utils.runAndCheckException. Checks for type and final thrower. + record ExChecker(Class ex, String caller) + implements Consumer { + ExChecker { + Objects.requireNonNull(ex); + } + static ExChecker of(Class 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()); + } + } + } +} diff --git a/test/jdk/javax/crypto/KEM/RSA_KEM.java b/test/jdk/javax/crypto/KEM/RSA_KEM.java new file mode 100644 index 00000000000..c46ded77623 --- /dev/null +++ b/test/jdk/javax/crypto/KEM/RSA_KEM.java @@ -0,0 +1,501 @@ +/* + * Copyright (c) 2023, 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 8297878 + * @summary RSA_KEM example + * @modules java.base/sun.security.jca + * java.base/sun.security.rsa + * java.base/sun.security.util + */ +import sun.security.jca.JCAUtil; +import sun.security.rsa.RSACore; +import sun.security.util.*; + +import javax.crypto.*; +import javax.crypto.spec.*; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidParameterSpecException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +// This test implements RSA-KEM as described in RFC 5990. In this KEM, the +// sender configures the encapsulator with an RSAKEMParameterSpec object. +// This object is encoded as a byte array and included in the Encapsulated +// output. The receiver is then able to recover the same RSAKEMParameterSpec +// object from the encoding using an AlgorithmParameters implementation +// and use the object to configure the decapsulator. +public class RSA_KEM { + public static void main(String[] args) throws Exception { + Provider p = new ProviderImpl(); + RSAKEMParameterSpec[] kspecs = new RSAKEMParameterSpec[] { + RSAKEMParameterSpec.kdf1("SHA-256", "AES_128/KW/NoPadding"), + RSAKEMParameterSpec.kdf1("SHA-512", "AES_256/KW/NoPadding"), + RSAKEMParameterSpec.kdf2("SHA-256", "AES_128/KW/NoPadding"), + RSAKEMParameterSpec.kdf2("SHA-512", "AES_256/KW/NoPadding"), + RSAKEMParameterSpec.kdf3("SHA-256", new byte[10], "AES_128/KW/NoPadding"), + RSAKEMParameterSpec.kdf3("SHA-256", new byte[0], "AES_128/KW/NoPadding"), + RSAKEMParameterSpec.kdf3("SHA-512", new byte[0], "AES_128/KW/NoPadding"), + }; + for (RSAKEMParameterSpec kspec : kspecs) { + System.err.println("---------"); + System.err.println(kspec); + AlgorithmParameters d = AlgorithmParameters.getInstance("RSA-KEM", p); + d.init(kspec); + AlgorithmParameters s = AlgorithmParameters.getInstance("RSA-KEM", p); + s.init(d.getEncoded()); + AlgorithmParameterSpec spec = s.getParameterSpec(AlgorithmParameterSpec.class); + if (!spec.toString().equals(kspec.toString())) { + throw new RuntimeException(spec.toString()); + } + } + byte[] msg = "hello".getBytes(StandardCharsets.UTF_8); + byte[] iv = new byte[16]; + for (int size : List.of(1024, 2048)) { + KeyPairGenerator g = KeyPairGenerator.getInstance("RSA"); + g.initialize(size); + KeyPair kp = g.generateKeyPair(); + for (RSAKEMParameterSpec kspec : kspecs) { + SecretKey cek = KeyGenerator.getInstance("AES").generateKey(); + KEM kem1 = KEM.getInstance("RSA-KEM", p); + Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); + c.init(Cipher.ENCRYPT_MODE, cek, new IvParameterSpec(iv)); + byte[] ciphertext = c.doFinal(msg); + + KEM.Encapsulator e = kem1.newEncapsulator(kp.getPublic(), kspec, null); + KEM.Encapsulated enc = e.encapsulate(0, e.secretSize(), "AES"); + Cipher c2 = Cipher.getInstance(kspec.encAlg); + c2.init(Cipher.WRAP_MODE, enc.key()); + byte[] ek = c2.wrap(cek); + + AlgorithmParameters a = AlgorithmParameters.getInstance("RSA-KEM", p); + a.init(enc.params()); + KEM kem2 = KEM.getInstance("RSA-KEM", p); + KEM.Decapsulator d = kem2.newDecapsulator(kp.getPrivate(), a.getParameterSpec(AlgorithmParameterSpec.class)); + SecretKey k = d.decapsulate(enc.encapsulation(), 0, d.secretSize(), "AES"); + Cipher c3 = Cipher.getInstance(kspec.encAlg); + c3.init(Cipher.UNWRAP_MODE, k); + cek = (SecretKey) c3.unwrap(ek, "AES", Cipher.SECRET_KEY); + Cipher c4 = Cipher.getInstance("AES/CBC/PKCS5Padding"); + c4.init(Cipher.DECRYPT_MODE, cek, new IvParameterSpec(iv)); + byte[] cleartext = c4.doFinal(ciphertext); + + if (!Arrays.equals(cleartext, msg)) { + throw new RuntimeException(); + } + System.out.printf("%4d %20s - %11d %11d %11d %11d %s\n", + size, kspec, + e.secretSize(), e.encapsulationSize(), + d.secretSize(), d.encapsulationSize(), k.getAlgorithm()); + } + } + } + + static final String RSA_KEM = "1.2.840.113549.1.9.16.3.14"; + static final String KEM_RSA = "1.0.18033.2.2.4"; + + public static class ProviderImpl extends Provider { + public ProviderImpl() { + super("MYKEM", "1", "RSA-KEM"); + List alias = List.of(RSA_KEM, "OID." + RSA_KEM); + Map attrs = Map.of( + "SupportedKeyClasses", "java.security.interfaces.RSAKey"); + putService(new Service(this, "KEM", "RSA-KEM", + "RSA_KEM$KEMImpl", alias, attrs)); + putService(new Service(this, "AlgorithmParameters", "RSA-KEM", + "RSA_KEM$AlgorithmParametersImpl", alias, attrs)); + } + } + + public static class AlgorithmParametersImpl extends AlgorithmParametersSpi { + RSAKEMParameterSpec spec; + @Override + protected void engineInit(AlgorithmParameterSpec paramSpec) + throws InvalidParameterSpecException { + if (paramSpec instanceof RSAKEMParameterSpec rspec) { + spec = rspec; + } else { + throw new InvalidParameterSpecException(); + } + } + + @Override + protected void engineInit(byte[] params) throws IOException { + spec = decode(params); + } + + @Override + protected void engineInit(byte[] params, String format) throws IOException { + spec = decode(params); + } + + @Override + protected T engineGetParameterSpec( + Class paramSpec) throws InvalidParameterSpecException { + if (paramSpec.isAssignableFrom(RSAKEMParameterSpec.class)) { + return paramSpec.cast(spec); + } else { + throw new InvalidParameterSpecException(); + } + } + + @Override + protected byte[] engineGetEncoded() { + return encode(spec); + } + + @Override + protected byte[] engineGetEncoded(String format) { + return encode(spec); + } + + @Override + protected String engineToString() { + return spec == null ? "" : spec.toString(); + } + + static final ObjectIdentifier id_rsa_kem; + static final ObjectIdentifier id_kem_rsa; + static final ObjectIdentifier id_kdf1; + static final ObjectIdentifier id_kdf2; + static final ObjectIdentifier id_kdf3; + + static { + try { + id_rsa_kem = ObjectIdentifier.of("1.2.840.113549.1.9.16.3.14"); + id_kem_rsa = ObjectIdentifier.of("1.0.18033.2.2.4"); + id_kdf1 = ObjectIdentifier.of("1.3.133.16.840.9.44.1.0"); // fake + id_kdf2 = ObjectIdentifier.of("1.3.133.16.840.9.44.1.1"); + id_kdf3 = ObjectIdentifier.of("1.3.133.16.840.9.44.1.2"); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + static byte[] encode(RSAKEMParameterSpec spec) { + DerOutputStream kdf = new DerOutputStream() + .write(DerValue.tag_Sequence, new DerOutputStream() + .putOID(oid4(spec.kdfAlg)) + .write(DerValue.tag_Sequence, new DerOutputStream() + .putOID(oid4(spec.hashAlg)))) + .putInteger(spec.kdfLen()); + // The next line is not in RFC 5990 + if (spec.fixedInfo != null) { + kdf.putOctetString(spec.fixedInfo); + } + return new DerOutputStream() + .write(DerValue.tag_Sequence, new DerOutputStream() + .write(DerValue.tag_Sequence, new DerOutputStream() + .putOID(id_kem_rsa) + .write(DerValue.tag_Sequence, kdf)) + .write(DerValue.tag_Sequence, new DerOutputStream() + .putOID(oid4(spec.encAlg)))).toByteArray(); + } + + static RSAKEMParameterSpec decode(byte[] der) throws IOException { + String kdfAlg, encAlg, hashAlg; + int kdfLen; + byte[] fixedInfo; + DerInputStream d2 = new DerValue(der).toDerInputStream(); + DerInputStream d3 = d2.getDerValue().toDerInputStream(); + if (!d3.getOID().equals(id_kem_rsa)) { + throw new IOException("not id_kem_rsa"); + } + DerInputStream d4 = d3.getDerValue().toDerInputStream(); + DerInputStream d5 = d4.getDerValue().toDerInputStream(); + kdfLen = d4.getInteger(); + fixedInfo = d4.available() > 0 ? d4.getOctetString() : null; + d4.atEnd(); + ObjectIdentifier kdfOid = d5.getOID(); + if (kdfOid.equals(id_kdf1)) { + kdfAlg = "kdf1"; + } else if (kdfOid.equals(id_kdf2)) { + kdfAlg = "kdf2"; + } else if (kdfOid.equals(id_kdf3)) { + kdfAlg = "kdf3"; + } else { + throw new IOException("unknown kdf"); + } + DerInputStream d6 = d5.getDerValue().toDerInputStream(); + String hashOID = d6.getOID().toString(); + KnownOIDs k = KnownOIDs.findMatch(hashOID); + hashAlg = k == null ? hashOID : k.stdName(); + d6.atEnd(); + d5.atEnd(); + + d3.atEnd(); + DerInputStream d7 = d2.getDerValue().toDerInputStream(); + String encOID = d7.getOID().toString(); + KnownOIDs e = KnownOIDs.findMatch(encOID); + encAlg = e == null ? encOID : e.stdName(); + d7.atEnd(); + d2.atEnd(); + if (kdfLen != RSAKEMParameterSpec.kdfLen(encAlg)) { + throw new IOException("kdfLen does not match encAlg"); + } + return new RSAKEMParameterSpec(kdfAlg, hashAlg, fixedInfo, encAlg); + } + + static ObjectIdentifier oid4(String s) { + return switch (s) { + case "kdf1" -> id_kdf1; + case "kdf2" -> id_kdf2; + case "kdf3" -> id_kdf3; + default -> { + KnownOIDs k = KnownOIDs.findMatch(s); + if (k == null) throw new UnsupportedOperationException(); + yield ObjectIdentifier.of(k); + } + }; + } + } + + public static class RSAKEMParameterSpec implements AlgorithmParameterSpec { + private final String kdfAlg; + private final String hashAlg; + private final byte[] fixedInfo; + private final String encAlg; + + private RSAKEMParameterSpec(String kdfAlg, String hashAlg, byte[] fixedInfo, String encAlg) { + this.hashAlg = hashAlg; + this.kdfAlg = kdfAlg; + this.fixedInfo = fixedInfo == null ? null : fixedInfo.clone(); + this.encAlg = encAlg; + } + + public static RSAKEMParameterSpec kdf1(String hashAlg, String encAlg) { + return new RSAKEMParameterSpec("kdf1", hashAlg, null, encAlg); + } + public static RSAKEMParameterSpec kdf2(String hashAlg, String encAlg) { + return new RSAKEMParameterSpec("kdf2", hashAlg, null, encAlg); + } + public static RSAKEMParameterSpec kdf3(String hashAlg, byte[] fixedInfo, String encAlg) { + return new RSAKEMParameterSpec("kdf3", hashAlg, fixedInfo, encAlg); + } + + public int kdfLen() { + return RSAKEMParameterSpec.kdfLen(encAlg); + } + + public static int kdfLen(String encAlg) { + return Integer.parseInt(encAlg, 4, 7, 10) / 8; + } + + public String hashAlgorithm() { + return hashAlg; + } + public String kdfAlgorithm() { + return kdfAlg; + } + public byte[] fixedInfo() { + return fixedInfo == null ? null : fixedInfo.clone(); + } + + public String getEncAlg() { + return encAlg; + } + + @Override + public String toString() { + return String.format("[%s,%s,%s]", kdfAlg, hashAlg, encAlg); + } + } + + public static class KEMImpl implements KEMSpi { + + @Override + public KEMSpi.EncapsulatorSpi engineNewEncapsulator( + PublicKey pk, AlgorithmParameterSpec spec, SecureRandom secureRandom) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (!(pk instanceof RSAPublicKey rpk)) { + throw new InvalidKeyException("Not an RSA key"); + } + return Handler.newEncapsulator(spec, rpk, secureRandom); + } + + @Override + public KEMSpi.DecapsulatorSpi engineNewDecapsulator( + PrivateKey sk, AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (!(sk instanceof RSAPrivateCrtKey rsk)) { + throw new InvalidKeyException("Not an RSA key"); + } + return Handler.newDecapsulator(spec, rsk); + } + + static class Handler implements KEMSpi.EncapsulatorSpi, KEMSpi.DecapsulatorSpi { + + private final RSAPublicKey rpk; // not null for encapsulator + private final RSAPrivateKey rsk; // not null for decapsulator + private final RSAKEMParameterSpec kspec; // not null + private final SecureRandom sr; // not null for encapsulator + + Handler(AlgorithmParameterSpec spec, RSAPublicKey rpk, RSAPrivateCrtKey rsk, SecureRandom sr) + throws InvalidAlgorithmParameterException { + this.rpk = rpk; + this.rsk = rsk; + this.sr = sr; + if (spec != null) { + if (spec instanceof RSAKEMParameterSpec rs) { + this.kspec = rs; + } else { + throw new InvalidAlgorithmParameterException(); + } + } else { + this.kspec = RSAKEMParameterSpec + .kdf2("SHA-256", "AES_256/KW/NoPadding"); + } + } + + static Handler newEncapsulator(AlgorithmParameterSpec spec, RSAPublicKey rpk, SecureRandom sr) + throws InvalidAlgorithmParameterException { + if (sr == null) { + sr = JCAUtil.getDefSecureRandom(); + } + return new Handler(spec, rpk, null, sr); + } + + static Handler newDecapsulator(AlgorithmParameterSpec spec, RSAPrivateCrtKey rsk) + throws InvalidAlgorithmParameterException { + return new Handler(spec, null, rsk, null); + } + + @Override + public SecretKey engineDecapsulate(byte[] encapsulation, + int from, int to, String algorithm) + throws DecapsulateException { + Objects.checkFromToIndex(from, to, kspec.kdfLen()); + Objects.requireNonNull(algorithm, "null algorithm"); + Objects.requireNonNull(encapsulation, "null encapsulation"); + if (encapsulation.length != KeyUtil.getKeySize(rsk) / 8) { + throw new DecapsulateException("incorrect encapsulation size"); + } + try { + byte[] Z = RSACore.rsa(encapsulation, rsk, false); + return new SecretKeySpec(kdf(Z), from, to - from, algorithm); + } catch (BadPaddingException e) { + throw new DecapsulateException("cannot decrypt", e); + } + } + + @Override + public KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm) { + Objects.checkFromToIndex(from, to, kspec.kdfLen()); + Objects.requireNonNull(algorithm, "null algorithm"); + int nLen = rpk.getModulus().bitLength(); + int nSize = (nLen + 7) / 8; + BigInteger z; + int tried = 0; + while (true) { + z = new BigInteger(nLen, sr); + if (z.compareTo(rpk.getModulus()) < 0) { + break; + } + if (tried++ > 20) { + throw new ProviderException("Cannot get good random number"); + } + } + byte[] Z = z.toByteArray(); + if (Z.length > nSize) { + Z = Arrays.copyOfRange(Z, Z.length - nSize, Z.length); + } else if (Z.length < nSize) { + byte[] tmp = new byte[nSize]; + System.arraycopy(Z, 0, tmp, nSize - Z.length, Z.length); + Z = tmp; + } + byte[] c; + try { + c = RSACore.rsa(Z, rpk); + } catch (BadPaddingException e) { + throw new AssertionError(e); + } + return new KEM.Encapsulated( + new SecretKeySpec(kdf(Z), from, to - from, algorithm), + c, AlgorithmParametersImpl.encode(kspec)); + } + + byte[] kdf(byte[] input) { + String hashAlg = kspec.hashAlgorithm(); + MessageDigest md; + try { + md = MessageDigest.getInstance(hashAlg); + } catch (NoSuchAlgorithmException e) { + throw new ProviderException(e); + } + String kdfAlg = kspec.kdfAlgorithm(); + byte[] fixedInput = kspec.fixedInfo(); + int length = kspec.kdfLen(); + + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + int n = kdfAlg.equals("kdf1") ? 0 : 1; + while (true) { + switch (kdfAlg) { + case "kdf1", "kdf2" -> { + md.update(input); + md.update(u32str(n)); + } + case "kdf3" -> { + md.update(u32str(n)); + md.update(input); + md.update(fixedInput); + } + default -> throw new ProviderException(); + } + bout.writeBytes(md.digest()); + if (bout.size() > length) break; + n++; + } + byte[] result = bout.toByteArray(); + return result.length == length + ? result + : Arrays.copyOf(result, length); + } + + @Override + public int engineSecretSize() { + return kspec.kdfLen(); + } + + @Override + public int engineEncapsulationSize() { + return KeyUtil.getKeySize(rsk == null ? rpk : rsk) / 8; + } + } + } + + static byte[] u32str(int i) { + return new byte[] { + (byte)(i >> 24), (byte)(i >> 16), (byte)(i >> 8), (byte)i }; + } +}