WebKey.java

/*
 * Copyright © 2025 Indiana University
 * All rights reserved.
 *
 * BSD 3-Clause License
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * - Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 * 
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * 
 * - Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package edu.iu.crypt;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.security.AlgorithmParameters;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.interfaces.XECKey;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.NamedParameterSpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.ArrayDeque;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.stream.Stream;

import javax.crypto.SecretKey;

import edu.iu.IuException;
import edu.iu.IuObject;
import edu.iu.crypt.WebCryptoHeader.Param;
import edu.iu.crypt.WebEncryption.Encryption;

/**
 * Unifies algorithm support and maps a cryptographic key from JCE to JSON Web
 * Key.
 * 
 * @see <a href="https://datatracker.ietf.org/doc/html/rfc7517">JSON Web Key
 *      (JWK) RFC-7517</a>
 */
public interface WebKey extends WebKeyReference {

	/**
	 * Gets the {@link ECParameterSpec} for a standard parameter name.
	 * 
	 * @param name standard parameter name
	 * @return Elliptic Curve parameters
	 */
	static AlgorithmParameterSpec algorithmParams(String name) {
		return IuObject.convert(name, a -> IuException.unchecked(() -> {
			if (Set.of("secp256r1", "secp384r1", "secp521r1").contains(a)) {
				final var algorithmParamters = AlgorithmParameters.getInstance("EC");
				algorithmParamters.init(new ECGenParameterSpec(a));
				return algorithmParamters.getParameterSpec(ECParameterSpec.class);
			} else if (Set.of("Ed25519", "Ed448", "X25519", "X448").contains(a))
				return (AlgorithmParameterSpec) IuException
						.unchecked(() -> NamedParameterSpec.class.getField(a.toUpperCase()).get(null));
			else
				return null;
		}));
	}

	/**
	 * Gets the {@link AlgorithmParameterSpec} from a key.
	 * 
	 * @param key key
	 * @return {@link AlgorithmParameterSpec}
	 */
	static AlgorithmParameterSpec algorithmParams(Key key) {
		if (key == null //
				|| key.getAlgorithm() == null //
				|| key.getAlgorithm().startsWith("RSA"))
			return null;

		if (key instanceof ECKey)
			return ((ECKey) key).getParams();
		if (key instanceof XECKey)
			return ((XECKey) key).getParams();
		else // EdEC is the last supported type; throws IllegalStateException on JDK 11
				// TODO switch from reflection to compiled cast for source level to 17+
			return (NamedParameterSpec) IuException.uncheckedInvocation(() -> ClassLoader.getPlatformClassLoader()
					.loadClass("java.security.interfaces.EdECKey").getMethod("getParams").invoke(key));
	}

	/**
	 * Enumerates key type.
	 * 
	 * @see <a href="https://datatracker.ietf.org/doc/html/rfc7518#section-6.1">RFC
	 *      7518 JWA Section 6.1</a>
	 */
	enum Type {
		/**
		 * NIST P-256 Elliptic Curve.
		 */
		EC_P256("EC", "P-256", "secp256r1"),

		/**
		 * NIST P-384 Elliptic Curve.
		 */
		EC_P384("EC", "P-384", "secp384r1"),

		/**
		 * NIST P-521 Elliptic Curve.
		 */
		EC_P521("EC", "P-521", "secp521r1"),

		/**
		 * Edwards 25519 Elliptic Curve, for {@link Use#SIGN}.
		 * 
		 * @see <a href="https://www.rfc-editor.org/rfc/rfc8037.html">RFC-8037</a>
		 */
		ED25519("OKP", "Ed25519", "Ed25519"),

		/**
		 * Edwards 448 Elliptic Curve, for {@link Use#SIGN}.
		 * 
		 * @see <a href="https://www.rfc-editor.org/rfc/rfc8037.html">RFC-8037</a>
		 */
		ED448("OKP", "Ed448", "Ed448"),

		/**
		 * ECDH X25519 Elliptic Curve, for {@link Use#ENCRYPT}.
		 * 
		 * @see <a href="https://www.rfc-editor.org/rfc/rfc8037.html">RFC-8037</a>
		 */
		X25519("OKP", "X25519", "X25519"),

		/**
		 * ECDH X448 Elliptic Curve, for {@link Use#ENCRYPT}.
		 * 
		 * @see <a href="https://www.rfc-editor.org/rfc/rfc8037.html">RFC-8037</a>
		 */
		X448("OKP", "X448", "X448"),

		/**
		 * RSA encryption or RSASSA-PKCS1-v1_5 signing, minimum 2048 bit.
		 */
		RSA("RSA", null, null),

		/**
		 * RSASSA-PSS signing, minimum 2048 bit.
		 */
		RSASSA_PSS("RSASSA-PSS", null, null),

		/**
		 * Raw symmetric key data (octet sequence).
		 */
		RAW("oct", null, null);

		/**
		 * Gets the value equivalent to the JWK kty attribute.
		 * 
		 * @param kty JWK kty attribute value
		 * @param crv JWK crv attribute value
		 * @return {@link Type}
		 */
		public static Type from(String kty, String crv) {
			return Stream.of(Type.values()).filter(a -> IuObject.equals(kty, a.kty) //
					&& IuObject.equals(crv, a.crv)).findFirst().orElse(null);
		}

		/**
		 * Gets the value equivalent to the JWK kty attribute.
		 * 
		 * @param algorithmParams Standard algorithm parameters name
		 * @return {@link Type}
		 */
		public static Type from(AlgorithmParameterSpec algorithmParams) {
			if (algorithmParams == null)
				return null;

			final Predicate<Type> specMatch;
			if (algorithmParams instanceof NamedParameterSpec) {
				final var namedSpec = (NamedParameterSpec) algorithmParams;
				specMatch = type -> {
					final var typeSpec = algorithmParams(type.algorithmParams);
					return (typeSpec instanceof NamedParameterSpec)
							&& ((NamedParameterSpec) typeSpec).getName().equals(namedSpec.getName());
				};
			} else
				specMatch = type -> algorithmParams.equals(algorithmParams(type.algorithmParams));

			return Stream.of(Type.values()).filter(specMatch).findFirst().orElse(null);
		}

		/**
		 * JSON kty attribute value.
		 */
		public final String kty;

		/**
		 * JSON crv attribute value.
		 */
		public final String crv;

		/**
		 * Standard algorithm parameter specification name.
		 */
		public final String algorithmParams;

		private Type(String kty, String crv, String algorithmParams) {
			this.kty = kty;
			this.crv = crv;
			this.algorithmParams = algorithmParams;
		}
	}

	/**
	 * Enumerates public key use.
	 */
	enum Use {
		/**
		 * Used for digital signing.
		 */
		SIGN("sig"),

		/**
		 * Used for encryption.
		 */
		ENCRYPT("enc");

		/**
		 * Gets the value equivalent to the JWK use attribute.
		 * 
		 * @param use JWK use attribute value
		 * @return {@link Use}
		 */
		public static Use from(String use) {
			return EnumSet.allOf(Use.class).stream().filter(a -> use.equals(a.use)).findFirst().get();
		}

		/**
		 * JSON use attribute value.
		 */
		public final String use;

		private Use(String use) {
			this.use = use;
		}
	}

	/**
	 * Enumerates key operations.
	 */
	enum Operation {
		/**
		 * Compute digital signature or MAC.
		 */
		SIGN("sign"),

		/**
		 * Verify digital signature or MAC.
		 */
		VERIFY("verify"),

		/**
		 * Encrypt content.
		 */
		ENCRYPT("encrypt"),

		/**
		 * Decrypt content and validate decryption.
		 */
		DECRYPT("decrypt"),

		/**
		 * Encrypt key.
		 */
		WRAP("wrapKey"),

		/**
		 * Decrypt key and validate decryption.
		 */
		UNWRAP("unwrapKey"),

		/**
		 * Derive key.
		 */
		DERIVE_KEY("deriveKey"),

		/**
		 * Derive bits not to be used as a key.
		 */
		DERIVE_BITS("deriveBits");

		/**
		 * Gets an item value equivalent to the JWK key_ops attribute.
		 * 
		 * @param keyOp key_ops item value
		 * @return {@link Operation}
		 */
		public static Operation from(String keyOp) {
			return EnumSet.allOf(Operation.class).stream().filter(a -> IuObject.equals(keyOp, a.keyOp)).findFirst()
					.get();
		}

		/**
		 * JSON key_ops item value
		 */
		public final String keyOp;

		private Operation(String keyOp) {
			this.keyOp = keyOp;
		}
	}

	/**
	 * Enumerates supported signature and encryption algorithms.
	 */
	enum Algorithm {
		/**
		 * HMAC symmetric key signature w/ SHA-256.
		 */
		HS256("HS256", "HmacSHA256", 256, new Type[] { Type.RAW }, Use.SIGN,
				new Operation[] { Operation.SIGN, Operation.VERIFY }, Set.of()),

		/**
		 * HMAC symmetric key signature w/ SHA-384.
		 */
		HS384("HS384", "HmacSHA384", 384, new Type[] { Type.RAW }, Use.SIGN,
				new Operation[] { Operation.SIGN, Operation.VERIFY }, Set.of()),

		/**
		 * HMAC symmetric key signature w/ SHA-512.
		 */
		HS512("HS512", "HmacSHA512", 512, new Type[] { Type.RAW }, Use.SIGN,
				new Operation[] { Operation.SIGN, Operation.VERIFY }, Set.of()),

		/**
		 * RSASSA-PKCS1-v1_5 using SHA-256.
		 */
		@Deprecated
		RS256("RS256", "SHA256withRSA", 256, new Type[] { Type.RSA }, Use.SIGN,
				new Operation[] { Operation.SIGN, Operation.VERIFY }, Set.of()),

		/**
		 * RSASSA-PKCS1-v1_5 using SHA-384.
		 */
		@Deprecated
		RS384("RS384", "SHA384withRSA", 384, new Type[] { Type.RSA }, Use.SIGN,
				new Operation[] { Operation.SIGN, Operation.VERIFY }, Set.of()),

		/**
		 * RSASSA-PKCS1-v1_5 using SHA-512.
		 */
		@Deprecated
		RS512("RS512", "SHA512withRSA", 512, new Type[] { Type.RSA }, Use.SIGN,
				new Operation[] { Operation.SIGN, Operation.VERIFY }, Set.of()),

		/**
		 * Elliptic Curve signature w/ SHA-256.
		 */
		ES256("ES256", "SHA256withECDSA", 256, new Type[] { Type.EC_P256, Type.EC_P384, Type.EC_P521 }, Use.SIGN,
				new Operation[] { Operation.SIGN, Operation.VERIFY }, Set.of()),

		/**
		 * Elliptic Curve signature w/ SHA-384.
		 */
		ES384("ES384", "SHA384withECDSA", 384, new Type[] { Type.EC_P384, Type.EC_P521, Type.EC_P256 }, Use.SIGN,
				new Operation[] { Operation.SIGN, Operation.VERIFY }, Set.of()),

		/**
		 * Elliptic Curve signature w/ SHA-512.
		 */
		ES512("ES512", "SHA512withECDSA", 512, new Type[] { Type.EC_P521, Type.EC_P256, Type.EC_P384 }, Use.SIGN,
				new Operation[] { Operation.SIGN, Operation.VERIFY }, Set.of()),

		/**
		 * Edwards Elliptic Curve Digital Signature Algorithm.
		 */
		EDDSA("EdDSA", "EdDSA", 0, new Type[] { Type.ED25519, Type.ED448 }, Use.SIGN,
				new Operation[] { Operation.SIGN, Operation.VERIFY }, Set.of()),

		/**
		 * RSASSA-PSS using SHA-256 and MGF1 with SHA-256.
		 */
		PS256("PS256", "RSASSA-PSS", 256, new Type[] { Type.RSASSA_PSS }, Use.SIGN,
				new Operation[] { Operation.SIGN, Operation.VERIFY }, Set.of()),

		/**
		 * RSASSA-PSS using SHA-384 and MGF1 with SHA-384.
		 */
		PS384("PS384", "RSASSA-PSS", 384, new Type[] { Type.RSASSA_PSS }, Use.SIGN,
				new Operation[] { Operation.SIGN, Operation.VERIFY }, Set.of()),

		/**
		 * RSASSA-PSS using SHA-512 and MGF1 with SHA-512.
		 */
		PS512("PS512", "RSASSA-PSS", 512, new Type[] { Type.RSASSA_PSS }, Use.SIGN,
				new Operation[] { Operation.SIGN, Operation.VERIFY }, Set.of()),

		/**
		 * RSAES-PKCS1-v1_5.
		 */
		@Deprecated
		RSA1_5("RSA1_5", "RSA/ECB/PKCS1Padding", 2048, new Type[] { Type.RSA }, Use.ENCRYPT,
				new Operation[] { Operation.WRAP, Operation.UNWRAP }, Set.of(Param.ENCRYPTION, Param.ZIP)),

		/**
		 * RSAES OAEP w/ default parameters.
		 */
		RSA_OAEP("RSA-OAEP", "RSA/ECB/OAEPWithSHA-1AndMGF1Padding", 2048, new Type[] { Type.RSA }, Use.ENCRYPT,
				new Operation[] { Operation.WRAP, Operation.UNWRAP }, Set.of(Param.ENCRYPTION, Param.ZIP)),

		/**
		 * RSAES OAEP w/ SHA-256 and MGF-1.
		 */
		RSA_OAEP_256("RSA-OAEP-256", "RSA/ECB/OAEPWithSHA-256AndMGF1Padding", 2048, new Type[] { Type.RSA },
				Use.ENCRYPT, new Operation[] { Operation.WRAP, Operation.UNWRAP }, Set.of(Param.ENCRYPTION, Param.ZIP)),

		/**
		 * AES-128 GCM Key Wrap.
		 * 
		 * @see <a href=
		 *      "https://datatracker.ietf.org/doc/html/rfc7518#section-4.6">RFC-7518 JWA
		 *      Section 4.6</a>
		 */
		A128GCMKW("A128GCMKW", "AES/GCM/NoPadding", 128, new Type[] { Type.RAW }, Use.ENCRYPT,
				new Operation[] { Operation.WRAP, Operation.UNWRAP },
				Set.of(Param.ENCRYPTION, Param.ZIP, Param.INITIALIZATION_VECTOR, Param.TAG)),

		/**
		 * AES-192 GCM Key Wrap.
		 * 
		 * @see <a href=
		 *      "https://datatracker.ietf.org/doc/html/rfc7518#section-4.6">RFC-7518 JWA
		 *      Section 4.6</a>
		 */
		A192GCMKW("A192GCMKW", "AES/GCM/NoPadding", 192, new Type[] { Type.RAW }, Use.ENCRYPT,
				new Operation[] { Operation.WRAP, Operation.UNWRAP },
				Set.of(Param.ENCRYPTION, Param.ZIP, Param.INITIALIZATION_VECTOR, Param.TAG)),
		/**
		 * AES-256 GCM Key Wrap.
		 * 
		 * @see <a href=
		 *      "https://datatracker.ietf.org/doc/html/rfc7518#section-4.6">RFC-7518 JWA
		 *      Section 4.6</a>
		 */
		A256GCMKW("A256GCMKW", "AES/GCM/NoPadding", 256, new Type[] { Type.RAW }, Use.ENCRYPT,
				new Operation[] { Operation.WRAP, Operation.UNWRAP },
				Set.of(Param.ENCRYPTION, Param.ZIP, Param.INITIALIZATION_VECTOR, Param.TAG)),

		/**
		 * AES-128 Key Wrap.
		 */
		A128KW("A128KW", "AESWrap", 128, new Type[] { Type.RAW }, Use.ENCRYPT,
				new Operation[] { Operation.WRAP, Operation.UNWRAP }, Set.of(Param.ENCRYPTION, Param.ZIP)),

		/**
		 * AES-192 Key Wrap.
		 */
		A192KW("A192KW", "AESWrap", 192, new Type[] { Type.RAW }, Use.ENCRYPT,
				new Operation[] { Operation.WRAP, Operation.UNWRAP }, Set.of(Param.ENCRYPTION, Param.ZIP)),

		/**
		 * AES-256 Key Wrap.
		 */
		A256KW("A256KW", "AESWrap", 256, new Type[] { Type.RAW }, Use.ENCRYPT,
				new Operation[] { Operation.WRAP, Operation.UNWRAP }, Set.of(Param.ENCRYPTION, Param.ZIP)),

		/**
		 * Direct use (as CEK).
		 */
		DIRECT("dir", null, 256, new Type[] { Type.RAW }, Use.ENCRYPT,
				new Operation[] { Operation.ENCRYPT, Operation.DECRYPT }, Set.of(Param.ENCRYPTION, Param.ZIP)),

		/**
		 * Elliptic Curve Diffie-Hellman Ephemeral Static key agreement.
		 * 
		 * @see <a href=
		 *      "https://datatracker.ietf.org/doc/html/rfc7518#section-4.6">RFC-7518 JWA
		 *      Section 4.6</a>
		 */
		ECDH_ES("ECDH-ES", "ECDH", 0, new Type[] { Type.X25519, Type.X448, Type.EC_P256, Type.EC_P384, Type.EC_P521 },
				Use.ENCRYPT, new Operation[] { Operation.DERIVE_KEY },
				Set.of(Param.ENCRYPTION, Param.ZIP, Param.EPHEMERAL_PUBLIC_KEY, Param.PARTY_UINFO, Param.PARTY_VINFO)),

		/**
		 * Elliptic Curve Diffie-Hellman Ephemeral Static key agreement w/ AES-128 Key
		 * Wrap.
		 * 
		 * @see <a href=
		 *      "https://datatracker.ietf.org/doc/html/rfc7518#section-4.6">RFC-7518 JWA
		 *      Section 4.6</a>
		 */
		ECDH_ES_A128KW("ECDH-ES+A128KW", "ECDH", 128,
				new Type[] { Type.X25519, Type.X448, Type.EC_P256, Type.EC_P384, Type.EC_P521 }, Use.ENCRYPT,
				new Operation[] { Operation.DERIVE_KEY },
				Set.of(Param.ENCRYPTION, Param.ZIP, Param.EPHEMERAL_PUBLIC_KEY, Param.PARTY_UINFO, Param.PARTY_VINFO)),

		/**
		 * Elliptic Curve Diffie-Hellman Ephemeral Static key agreement w/ AES-192 Key
		 * Wrap.
		 * 
		 * @see <a href=
		 *      "https://datatracker.ietf.org/doc/html/rfc7518#section-4.6">RFC-7518 JWA
		 *      Section 4.6</a>
		 */
		ECDH_ES_A192KW("ECDH-ES+A192KW", "ECDH", 192,
				new Type[] { Type.X25519, Type.X448, Type.EC_P256, Type.EC_P384, Type.EC_P521 }, Use.ENCRYPT,
				new Operation[] { Operation.DERIVE_KEY },
				Set.of(Param.ENCRYPTION, Param.ZIP, Param.EPHEMERAL_PUBLIC_KEY, Param.PARTY_UINFO, Param.PARTY_VINFO)),

		/**
		 * Elliptic Curve Diffie-Hellman Ephemeral Static key agreement w/ AES-256 Key
		 * Wrap.
		 * 
		 * @see <a href=
		 *      "https://datatracker.ietf.org/doc/html/rfc7518#section-4.6">RFC-7518 JWA
		 *      Section 4.6</a>
		 */
		ECDH_ES_A256KW("ECDH-ES+A256KW", "ECDH", 256, new Type[] { Type.EC_P521, Type.EC_P256, Type.EC_P384 },
				Use.ENCRYPT, new Operation[] { Operation.DERIVE_KEY },
				Set.of(Param.ENCRYPTION, Param.ZIP, Param.EPHEMERAL_PUBLIC_KEY, Param.PARTY_UINFO, Param.PARTY_VINFO)),

		/**
		 * PBKDF2 with HMAC SHA-256 and AES128 key wrap.
		 */
		PBES2_HS256_A128KW("PBES2-HS256+A128KW", "PBKDF2WithHmacSHA256", 128, new Type[] { Type.RAW }, Use.ENCRYPT,
				new Operation[] { Operation.WRAP, Operation.UNWRAP },
				Set.of(Param.ENCRYPTION, Param.ZIP, Param.PASSWORD_SALT, Param.PASSWORD_COUNT)),

		/**
		 * PBKDF2 with HMAC SHA-384 and AES192 key wrap.
		 */
		PBES2_HS384_A192KW("PBES2-HS384+A192KW", "PBKDF2WithHmacSHA384", 192, new Type[] { Type.RAW }, Use.ENCRYPT,
				new Operation[] { Operation.WRAP, Operation.UNWRAP },
				Set.of(Param.ENCRYPTION, Param.ZIP, Param.PASSWORD_SALT, Param.PASSWORD_COUNT)),

		/**
		 * PBKDF2 with HMAC SHA-512 and AES192 key wrap.
		 */
		PBES2_HS512_A256KW("PBES2-HS512+A256KW", "PBKDF2WithHmacSHA512", 256, new Type[] { Type.RAW }, Use.ENCRYPT,
				new Operation[] { Operation.WRAP, Operation.UNWRAP },
				Set.of(Param.ENCRYPTION, Param.ZIP, Param.PASSWORD_SALT, Param.PASSWORD_COUNT));

		/**
		 * Gets the value equivalent to the JWK alg attribute.
		 * 
		 * @param alg alg attribute value
		 * @return {@link Operation}
		 */
		public static Algorithm from(String alg) {
			return EnumSet.allOf(Algorithm.class).stream().filter(a -> IuObject.equals(alg, a.alg)).findFirst().get();
		}

		/**
		 * JSON alg attribute value.
		 */
		public final String alg;

		/**
		 * JCE signature or key agreement algorithm name.
		 */
		public final String algorithm;

		/**
		 * Encryption key or signature hash size.
		 */
		public final int size;

		/**
		 * Key type associated with this algorithm.
		 */
		public final Type[] type;

		/**
		 * Key usage associated with this algorithm.
		 */
		public final Use use;

		/**
		 * Key usage associated with this algorithm.
		 */
		public final Operation[] keyOps;

		/**
		 * Set of encryption parameters used by this algorithm.
		 */
		public final Set<Param> encryptionParams;

		private Algorithm(String alg, String algorithm, int size, Type[] type, Use use, Operation[] keyOps,
				Set<Param> encryptionParams) {
			this.alg = alg;
			this.algorithm = algorithm;
			this.size = size;
			this.type = type;
			this.use = use;
			this.keyOps = keyOps;
			this.encryptionParams = encryptionParams;
		}
	}

	/**
	 * Builder interface for creating {@link WebKey} instances.
	 * 
	 * @param <B> builder type
	 */
	interface Builder<B extends Builder<B>> extends WebKeyReference.Builder<B> {
		/**
		 * Sets the key type.
		 * 
		 * @param type key type
		 * @return this
		 */
		B type(Type type);

		/**
		 * Sets the public key use.
		 *
		 * @param use public key use
		 * @return this
		 */
		B use(Use use);

		/**
		 * Sets the key operations.
		 * 
		 * @param ops key operations
		 * @return this
		 */
		B ops(Operation... ops);

		/**
		 * Generates a public/private key pair for the algorithm specified by
		 * {@link #algorithm(Algorithm)} using the default size.
		 * 
		 * @return this
		 */
		B ephemeral();

		/**
		 * Generates a public/private key pair or secret key without setting
		 * {@link #algorithm}.
		 * 
		 * @param algorithm algorithm the key will be used with
		 * @return this
		 */
		B ephemeral(Algorithm algorithm);

		/**
		 * Generates an ephemeral content encryption key.
		 * 
		 * @param encryption content encryption algorithm
		 * @return this
		 */
		B ephemeral(Encryption encryption);

		/**
		 * Adds raw key data.
		 * 
		 * @param key raw key data
		 * @return this
		 */
		B key(byte[] key);

		/**
		 * Adds public key parameters.
		 * 
		 * @param publicKey public key
		 * @return this
		 */
		B key(PublicKey publicKey);

		/**
		 * Adds private key parameters.
		 * 
		 * @param privateKey private key
		 * @return this
		 */
		B key(PrivateKey privateKey);

		/**
		 * Sets both public and private keys from a {@link KeyPair}.
		 * 
		 * @param keyPair key pair;
		 * @return this
		 */
		B key(KeyPair keyPair);

		/**
		 * Builds the web key.
		 * 
		 * @return {@link WebKey}
		 */
		WebKey build();
	}

	/**
	 * Verifies encoded key data is correct for the key type, use, algorithm, and
	 * X.509 certificate chain.
	 * 
	 * @param webKey {@link WebKey}
	 * @return {@link PublicKey} resolved from the web key, or null if no public key
	 *         was resolved; private and raw key values will be verified as valid
	 *         for the key type and/or public key, and <em>may</em> continue to be
	 *         accessed from the original web key as needed.
	 * @throws IllegalArgumentException if the key is invalid
	 */
	static PublicKey verify(WebKey webKey) {
		final var key = webKey.getKey();
		final var type = Objects.requireNonNull(webKey.getType(), "Key type is required");

		final var algorithm = webKey.getAlgorithm();
		if (algorithm != null //
				&& !Stream.of(algorithm.type).anyMatch(type::equals))
			throw new IllegalArgumentException("Illegal type " + type + " for algorithm " + algorithm);

		final var use = webKey.getUse();
		if (use != null //
				&& algorithm != null //
				&& !use.equals(algorithm.use))
			throw new IllegalArgumentException("Illegal use " + use + " for algorithm " + algorithm);

		final var ops = webKey.getOps();
		if (ops != null) {
			if (ops.size() > 2)
				throw new IllegalArgumentException("Illegal ops " + ops);
			else if (ops.size() == 2) {
				BiConsumer<Operation, Operation> checkPair = (a, b) -> {
					if (ops.contains(a) != (b != null && ops.contains(b)))
						throw new IllegalArgumentException("Illegal ops " + ops);
				};
				checkPair.accept(Operation.SIGN, Operation.VERIFY);
				checkPair.accept(Operation.ENCRYPT, Operation.DECRYPT);
				checkPair.accept(Operation.WRAP, Operation.UNWRAP);
				checkPair.accept(Operation.DERIVE_BITS, null);
				checkPair.accept(Operation.DERIVE_KEY, null);
			}

			if (algorithm != null //
					&& !Set.of(algorithm.keyOps).containsAll(ops))
				throw new IllegalArgumentException("Illegal ops " + ops + " for algorithm " + algorithm);

			if (use != null)
				if (ops.contains(Operation.SIGN) //
						|| ops.contains(Operation.VERIFY)) {
					if (use.equals(Use.ENCRYPT))
						throw new IllegalArgumentException("Illegal ops " + ops + " for use " + use);
				} else if (use.equals(Use.SIGN))
					throw new IllegalArgumentException("Illegal ops " + ops + " for use " + use);
		}

		final var cert = IuObject.convert(WebCertificateReference.verify(webKey), a -> a[0]);

		if (type.equals(Type.RAW)) {
			IuObject.require(webKey.getPrivateKey(), Objects::isNull, () -> "Unexpected private key");
			IuObject.require(webKey.getPublicKey(), Objects::isNull, () -> "Unexpected public key");
			IuObject.require(cert, Objects::isNull, () -> "Unexpected certificate");
			return null;
		}

		if (key != null)
			throw new IllegalArgumentException("Unexpected raw key data for " + type);

		var publicKey = IuObject.first(webKey.getPublicKey(), //
				IuObject.convert(cert, X509Certificate::getPublicKey), //
				() -> "public key doesn't match X.509 certificate");
		var params = algorithmParams(publicKey);

		final var privateKey = webKey.getPrivateKey();
		final var privateParams = algorithmParams(privateKey);
		if (params == null)
			params = privateParams;
		else if (params instanceof NamedParameterSpec) {
			final var namedSpec = (NamedParameterSpec) params;
			if (privateParams != null && //
					!namedSpec.getName().equals(((NamedParameterSpec) privateParams).getName()))
				throw new IllegalArgumentException("parameter spec mismatch");
		} else if (privateParams != null && //
				!params.equals(privateParams))
			throw new IllegalArgumentException("parameter spec mismatch");

		if ((publicKey instanceof RSAPublicKey) || (privateKey instanceof RSAPrivateKey)) {
			final var rsaPrivate = IuObject.requireType(RSAPrivateKey.class, privateKey);
			final var rsaPublic = IuObject.requireType(RSAPublicKey.class, publicKey);
			if (rsaPrivate != null)
				if (rsaPublic != null) {
					if (!rsaPrivate.getModulus().equals(rsaPublic.getModulus()))
						throw new IllegalArgumentException("RSA public key modulus doesn't match private key");
					else if ((rsaPrivate instanceof RSAPrivateCrtKey) //
							&& !((RSAPrivateCrtKey) rsaPrivate).getPublicExponent()
									.equals(rsaPublic.getPublicExponent()))
						throw new IllegalArgumentException("RSA public key exponent doesn't match private key");
				} else if (rsaPrivate instanceof RSAPrivateCrtKey)
					publicKey = IuException.unchecked(
							() -> (RSAPublicKey) KeyFactory.getInstance(type.kty).generatePublic(new RSAPublicKeySpec(
									rsaPrivate.getModulus(), ((RSAPrivateCrtKey) rsaPrivate).getPublicExponent())));
		} else if ((publicKey != null || privateKey != null) && params == null)
			throw new IllegalArgumentException("Missing algorithm parameters");

		if (ops != null) {
			if (ops.contains(Operation.ENCRYPT) || ops.contains(Operation.DECRYPT))
				throw new IllegalArgumentException("Secret key required by ops " + ops);
			if (publicKey == null && (ops.contains(Operation.WRAP) //
					|| ops.contains(Operation.VERIFY)))
				throw new IllegalArgumentException("Public key required by ops " + ops);
			if (privateKey == null && (ops.contains(Operation.UNWRAP) //
					|| ops.contains(Operation.SIGN)))
				throw new IllegalArgumentException("Private key required by ops " + ops);
			if (ops.contains(Operation.DERIVE_KEY) && privateKey == null && publicKey == null)
				throw new IllegalArgumentException("Public or private key required by ops " + ops);
		}

		return publicKey;
	}

	/**
	 * Creates a new builder.
	 * 
	 * @param key JCE key
	 * @return {@link Builder}
	 */
	static Builder<?> builder(Key key) {
		if (key instanceof SecretKey)
			return WebKey.builder(Type.RAW).key(key.getEncoded());

		final WebKey.Builder<?> jwkBuilder;
		final var params = WebKey.algorithmParams(key);
		if (params == null)
			jwkBuilder = WebKey.builder(Type.from(key.getAlgorithm(), null));
		else
			jwkBuilder = WebKey.builder(Objects.requireNonNull(Type.from(params), params.toString()));

		if (key instanceof PrivateKey)
			jwkBuilder.key((PrivateKey) key);
		else
			jwkBuilder.key((PublicKey) key);

		return jwkBuilder;
	}

	/**
	 * Creates a new {@link Builder}.
	 * 
	 * @param type key type
	 * @return {@link Builder}
	 */
	static Builder<?> builder(Type type) {
		return Init.SPI.getJwkBuilder(type);
	}

	/**
	 * Creates an ephemeral key for use as JWE recipient or JWS issuer.
	 * 
	 * <p>
	 * Ephemeral keys are generated using JDK 11 compliant <a href=
	 * "https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html">
	 * standard algorithms</a> with {@link Security#getProviders() registered JCE
	 * providers}
	 * </p>
	 * 
	 * @param algorithm key algorithm
	 * @return JWE recipient or JWS issuer key
	 */
	static Builder<?> builder(Algorithm algorithm) {
		return builder(algorithm.type[0]).algorithm(algorithm);
	}

	/**
	 * Creates an ephemeral content encryption key, for use with
	 * {@link Algorithm#DIRECT}.
	 * 
	 * <p>
	 * Ephemeral keys are generated using JDK 11 compliant <a href=
	 * "https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html">
	 * standard algorithms</a> with {@link Security#getProviders() registered JCE
	 * providers}
	 * </p>
	 * 
	 * @param encryption encryption algorithm
	 * @return content encryption key
	 */
	static WebKey ephemeral(Encryption encryption) {
		return builder(Type.RAW).ephemeral(encryption).build();
	}

	/**
	 * Creates an ephemeral key for use as JWE recipient or JWS issuer.
	 * 
	 * <p>
	 * Ephemeral keys are generated using JDK 11 compliant <a href=
	 * "https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html">
	 * standard algorithms</a> with {@link Security#getProviders() registered JCE
	 * providers}
	 * </p>
	 * 
	 * @param algorithm key algorithm
	 * @return JWE recipient or JWS issuer key
	 */
	static WebKey ephemeral(Algorithm algorithm) {
		return builder(algorithm.type[0]).ephemeral(algorithm).build();
	}

	/**
	 * Parses a JSON Web Key (JWK).
	 * 
	 * @param jwk JSON Web Key
	 * @return {@link WebKey}
	 */
	static WebKey parse(String jwk) {
		return Init.SPI.parseJwk(jwk);
	}

	/**
	 * Parses a JSON Web Key Set (JWKS).
	 * 
	 * @param jwks serialized JWKS
	 * @return parsed key set
	 */
	static Iterable<? extends WebKey> parseJwks(String jwks) {
		return Init.SPI.parseJwks(jwks);
	}

	/**
	 * Reads at least one PEM-encoded X509 certificate, and optionally a private
	 * key, and returns a JWK partial-key representation.
	 * 
	 * @param pem PEM-encoded certificate(s) and optional private key
	 * @return {@link WebKey}
	 */
	static WebKey pem(String pem) {
		final Queue<X509Certificate> certs = new ArrayDeque<>();
		PemEncoded privateKey = null;

		final var parsed = PemEncoded.parse(pem);
		while (parsed.hasNext()) {
			final var encoded = parsed.next();
			final var keyType = encoded.getKeyType();
			if (keyType.equals(PemEncoded.KeyType.CERTIFICATE))
				certs.offer(encoded.asCertificate());
			else if (keyType.equals(PemEncoded.KeyType.PRIVATE_KEY))
				privateKey = IuObject.once(privateKey, encoded);
		}

		final var publicKey = certs.peek().getPublicKey();
		final var builder = WebKey.builder(publicKey);
		builder.cert(certs.toArray(X509Certificate[]::new));
		if (privateKey != null)
			builder.key(privateKey.asPrivate(publicKey.getAlgorithm()));
		return builder.build();
	}

	/**
	 * Reads a JSON Web Key Set (JWKS).
	 * 
	 * @param jwks serialized JWKS
	 * @return parsed key set
	 */
	static Iterable<? extends WebKey> readJwks(URI jwks) {
		return Init.SPI.readJwks(jwks);
	}

	/**
	 * Reads a JSON Web Key Set (JWKS).
	 * 
	 * @param jwks serialized JWKS
	 * @return parsed key set
	 */
	static Iterable<? extends WebKey> readJwks(InputStream jwks) {
		return Init.SPI.readJwks(jwks);
	}

	/**
	 * Serializes {@link WebKey}s as a JSON Web Key Set.
	 * 
	 * @param webKeys {@link WebKey}s
	 * @return serialized JWKS
	 */
	static String asJwks(Iterable<? extends WebKey> webKeys) {
		return Init.SPI.asJwks(webKeys);
	}

	/**
	 * Writes {@link WebKey} as a JSON Web Key.
	 * 
	 * @param webKeys {@link WebKey}s
	 * @param out     {@link OutputStream}
	 */
	static void writeJwks(Iterable<? extends WebKey> webKeys, OutputStream out) {
		Init.SPI.writeJwks(webKeys, out);
	}

	/**
	 * Returns a copy of this key for which {@link #getPrivateKey()} and
	 * {@link #getKey()} always return null, and for which the source data backing
	 * these methods is not populated.
	 * 
	 * <p>
	 * If these methods would already return null, this key is returned as-is.
	 * </p>
	 * 
	 * @return this key, or a copy that omits secret and private key data
	 */
	WebKey wellKnown();

	/**
	 * Gets the key type.
	 * 
	 * @return key type
	 */
	Type getType();

	/**
	 * Gets the public key use.
	 * 
	 * @return public key use.
	 */
	Use getUse();

	/**
	 * Gets the key operations.
	 * 
	 * @return key operations
	 */
	Set<Operation> getOps();

	/**
	 * Gets the raw key data for use when {@link Type#RAW}.
	 * 
	 * @return raw key data
	 */
	byte[] getKey();

	/**
	 * Gets the JCE private key implementation.
	 * 
	 * @return {@link PrivateKey}
	 */
	PrivateKey getPrivateKey();

	/**
	 * Gets the JCE public key implementation.
	 * 
	 * @return {@link PublicKey}
	 */
	PublicKey getPublicKey();

}