JweRecipient.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 iu.crypt;
import java.nio.ByteBuffer;
import java.security.interfaces.RSAPrivateKey;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Objects;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import edu.iu.IuException;
import edu.iu.IuObject;
import edu.iu.IuText;
import edu.iu.client.IuJson;
import edu.iu.client.IuJsonAdapter;
import edu.iu.crypt.WebEncryption.Encryption;
import edu.iu.crypt.WebEncryptionRecipient;
import edu.iu.crypt.WebKey;
import edu.iu.crypt.WebKey.Algorithm;
import jakarta.json.JsonObject;
import jakarta.json.JsonValue;
/**
* Represents a recipient of a {@link Jwe} encrypted message.
*/
public class JweRecipient implements WebEncryptionRecipient {
static {
IuObject.assertNotOpen(JweRecipient.class);
}
private final Jose header;
private final byte[] encryptedKey;
/**
* Constructor.
*
* @param header header
* @param encryptedKey encrypted key
*/
JweRecipient(Jose header, byte[] encryptedKey) {
this.header = header;
this.encryptedKey = encryptedKey;
}
/**
* Constructor.
*
* @param protectedHeader protected header parameters
* @param sharedHeader shared header parameters
* @param recipient recipient parameters
*/
JweRecipient(JsonObject protectedHeader, JsonObject sharedHeader, JsonObject recipient) {
this(Jose.from(protectedHeader, sharedHeader,
IuJson.get(recipient, "header", IuJsonAdapter.from(JsonValue::asJsonObject))),
IuJson.get(recipient, "encrypted_key", CryptJsonAdapters.B64URL));
}
@Override
public Jose getHeader() {
return header;
}
@Override
public byte[] getEncryptedKey() {
return encryptedKey;
}
/**
* Computes the agreed-upon key for the Elliptic Curve Diffie-Hellman algorithm.
*
* @param encryption content encryption algorithm
* @param recipientPrivateKey recipient's private key
*
* @return agreed-upon key
* @see <a href=
* "https://datatracker.ietf.org/doc/html/rfc7518#section-4.6">RFC-7518 JWA
* Section 4.6</a>
* @see <a href=
* "https://datatracker.ietf.org/doc/html/rfc7516#section-5.1">RFC-7516 JWE
* Section 5.1 #3</a>
*/
byte[] agreedUponKey(Encryption encryption, WebKey recipientPrivateKey) {
final Jwk epk = Objects.requireNonNull(header.getExtendedParameter("epk"),
"epk required for " + header.getAlgorithm());
if (!epk.getType().equals(recipientPrivateKey.getType()))
throw new IllegalArgumentException("Private key type doesn't match epk");
final byte[] uinfo = header.getExtendedParameter("apu");
final byte[] vinfo = header.getExtendedParameter("apv");
final var algorithm = header.getAlgorithm();
final int keyDataLen;
final byte[] algId;
if (algorithm.equals(Algorithm.ECDH_ES)) {
keyDataLen = encryption.size;
algId = IuText.ascii(encryption.enc);
} else {
keyDataLen = algorithm.size;
algId = IuText.ascii(algorithm.alg);
}
final String keyAlg;
final var type = recipientPrivateKey.getType();
if (type.kty.equals("EC"))
keyAlg = "ECDH";
else
keyAlg = type.algorithmParams;
return JweRecipientBuilder.agreedUponKey(recipientPrivateKey.getPrivateKey(), epk.getPublicKey(), keyAlg, algId,
uinfo, vinfo, keyDataLen);
}
/**
* Gets the passphrase-derived key to use with PBKDF2 key derivation defined by
* <a href="https://datatracker.ietf.org/doc/html/rfc8018">PKCS#5</a>.
*
* @param passphrase passphrase
* @return 128-bit derived key data suitable for use with AESWrap
*/
byte[] passphraseDerivedKey(String passphrase) {
final var algorithm = header.getAlgorithm();
final var alg = IuText.utf8(algorithm.alg);
final byte[] p2s = Objects.requireNonNull(header.getExtendedParameter("p2s"), "p2s required for " + algorithm);
if (p2s.length < 8)
// RFC-7518 JWA #4.8.1.1:
// A Salt Input value containing 8 or more octets MUST be used.
throw new IllegalArgumentException("p2s must contain at least 8 bytes");
// ASVS4 #2.4.3: Verify that if PBKDF2 is used, the iteration count SHOULD be as
// large as verification server performance will allow, typically at least
// 100,000 iterations. (C6)
// RFC-7518 JWA #4.8.1.2: The iteration count adds computational expense,
// ideally compounded by the possible range of keys introduced by the salt. A
// minimum iteration count of 1000 is RECOMMENDED.
// For interoperability, allow p2c low as 1000 for decryption only
final int p2c = Objects.requireNonNull(header.getExtendedParameter("p2c"), "p2c required for " + algorithm);
if (p2c < 1000)
throw new IllegalArgumentException("p2c must contain be at least 1000");
final var saltValue = ByteBuffer.wrap(new byte[alg.length + 1 + p2s.length]);
saltValue.put(alg);
saltValue.put((byte) 0);
saltValue.put(p2s);
return IuException
.unchecked(() -> SecretKeyFactory.getInstance(algorithm.algorithm)
.generateSecret(new PBEKeySpec(passphrase.toCharArray(), saltValue.array(), p2c, 128)))
.getEncoded();
}
/**
* Decrypts the content encryption key (CEK)
*
* @param encryption content encryption algorithm
* @param privateKey private key
* @return content encryption key
*/
@SuppressWarnings("deprecation")
byte[] decryptCek(Encryption encryption, Jwk privateKey) {
// 5.2#7 Verify that the JWE uses a key known to the recipient.
final var recipientPublicKey = header.wellKnown();
if (recipientPublicKey != null //
&& recipientPublicKey.getPublicKey() != null //
&& !recipientPublicKey.represents(privateKey))
throw new IllegalArgumentException("Key is not valid for recipient");
final var algorithm = header.getAlgorithm();
if (algorithm.equals(Algorithm.DIRECT)) {
if (encryptedKey != null)
// 5.2#10 verify that the JWE Encrypted Key value is empty
throw new IllegalArgumentException("encrypted key must be empty for " + algorithm);
// 5.2#11 use shared key as CEK for direct encryption
final var cek = Objects.requireNonNull(privateKey.getKey(), "DIRECT requires a secret key");
if (cek.length != encryption.size / 8)
throw new IllegalArgumentException("Invalid key size for " + encryption);
return cek;
}
if (algorithm.equals(Algorithm.ECDH_ES))
// 5.2#10 verify that the JWE Encrypted Key value is an empty
if (encryptedKey != null)
throw new IllegalArgumentException("encrypted key must be empty for " + algorithm);
else
// 5.2#8 use agreed upon key as CEK for direct encryption
return agreedUponKey(encryption, privateKey);
// 5.2#9 encrypt CEK to the recipient
Objects.requireNonNull(encryptedKey, "encrypted key required for " + algorithm);
final byte[] cek;
if (EnumSet.of(Algorithm.A128KW, Algorithm.A192KW, Algorithm.A256KW).contains(algorithm))
// key wrapping
cek = IuException.unchecked(() -> {
final var key = new SecretKeySpec(privateKey.getKey(), "AES");
final var cipher = Cipher.getInstance(algorithm.algorithm);
cipher.init(Cipher.UNWRAP_MODE, key);
return ((SecretKey) cipher.unwrap(encryptedKey, "AES", Cipher.SECRET_KEY)).getEncoded();
});
else if (EnumSet.of(Algorithm.A128GCMKW, Algorithm.A192GCMKW, Algorithm.A256GCMKW).contains(algorithm))
// key wrapping
cek = IuException.unchecked(() -> {
final var key = new SecretKeySpec(privateKey.getKey(), "AES");
final byte[] iv = Objects.requireNonNull(header.getExtendedParameter("iv"),
"iv required for " + algorithm);
if (iv.length != 12)
throw new IllegalArgumentException("iv must be 96 bits");
final byte[] tag = Objects.requireNonNull(header.getExtendedParameter("tag"),
"tag required for " + algorithm);
if (tag.length != 16)
throw new IllegalArgumentException("tag must be 128 bits");
final var wrappedKey = Arrays.copyOf(encryptedKey, encryptedKey.length + 16);
System.arraycopy(tag, 0, wrappedKey, encryptedKey.length, 16);
final var cipher = Cipher.getInstance(algorithm.algorithm);
cipher.init(Cipher.UNWRAP_MODE, key, new GCMParameterSpec(128, iv));
return ((SecretKey) cipher.unwrap(wrappedKey, "AES", Cipher.SECRET_KEY)).getEncoded();
});
else if (EnumSet.of(Algorithm.RSA1_5, Algorithm.RSA_OAEP, Algorithm.RSA_OAEP_256).contains(algorithm))
// key encryption
cek = IuException.unchecked(() -> {
final var rsa = (RSAPrivateKey) privateKey.getPrivateKey();
final var keyCipher = Cipher.getInstance(algorithm.algorithm);
keyCipher.init(Cipher.DECRYPT_MODE, rsa);
return keyCipher.doFinal(encryptedKey);
});
else if (EnumSet.of(Algorithm.ECDH_ES_A128KW, Algorithm.ECDH_ES_A192KW, Algorithm.ECDH_ES_A256KW)
.contains(algorithm))
// key agreement with key wrapping
cek = IuException.unchecked(() -> {
final var key = new SecretKeySpec(agreedUponKey(encryption, privateKey), "AES");
final var cipher = Cipher.getInstance("AESWrap");
cipher.init(Cipher.UNWRAP_MODE, key);
return ((SecretKey) cipher.unwrap(encryptedKey, "AES", Cipher.SECRET_KEY)).getEncoded();
});
else // if (EnumSet.of(Algorithm.PBES2_HS256_A128KW, Algorithm.PBES2_HS384_A192KW,
// Algorithm.PBES2_HS512_A256KW)
// .contains(algorithm))
// password-based key derivation with key wrapping
cek = IuException.unchecked(() -> {
final var key = new SecretKeySpec(passphraseDerivedKey(IuText.utf8(privateKey.getKey())), "AES");
final var cipher = Cipher.getInstance("AESWrap");
cipher.init(Cipher.UNWRAP_MODE, key);
return ((SecretKey) cipher.unwrap(encryptedKey, "AES", Cipher.SECRET_KEY)).getEncoded();
});
return cek;
}
}