Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvement: EC (Elliptic-curve) Private Key Decryption Compatibility for Incoming Messages #403

Merged
merged 4 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 193 additions & 66 deletions Server/src/main/java/org/openas2/lib/helper/BCCryptoHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder;
import org.bouncycastle.cms.jcajce.JceKeyAgreeEnvelopedRecipient;
import org.bouncycastle.cms.jcajce.JceKeyAgreeRecipientInfoGenerator;
import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator;

import org.bouncycastle.cms.jcajce.ZlibCompressor;
import org.bouncycastle.cms.jcajce.ZlibExpanderProvider;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
Expand Down Expand Up @@ -67,16 +70,21 @@
import java.security.DigestInputStream;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECParameterSpec;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
Expand Down Expand Up @@ -198,79 +206,138 @@ public String calculateMIC(MimeBodyPart part, String digest, boolean includeHead
return micResult.toString();
}

public MimeBodyPart decrypt(MimeBodyPart part, Certificate cert, Key key) throws GeneralSecurityException, MessagingException, CMSException, IOException, SMIMEException {
// Make sure the data is encrypted
public MimeBodyPart decrypt(MimeBodyPart part, Certificate receiverCert, Key receiverKey)
throws GeneralSecurityException, MessagingException, CMSException, IOException, SMIMEException {

if (!isEncrypted(part)) {
throw new GeneralSecurityException("Content-Type indicates data isn't encrypted");
}

// Cast parameters to what BC needs
X509Certificate x509Cert = castCertificate(cert);

// Parse the MIME body into an SMIME envelope object
X509Certificate x509Cert = castCertificate(receiverCert);
SMIMEEnveloped envelope = new SMIMEEnveloped(part);

// Get the recipient object for decryption
if (logger.isDebugEnabled()) {
logger.debug("Extracted X500 info:: PRINCIPAL : " + x509Cert.getIssuerX500Principal() + " :: NAME : " + x509Cert.getIssuerX500Principal().getName());
}

X500Name x500Name = new X500Name(x509Cert.getIssuerX500Principal().getName());
KeyTransRecipientId certRecId = new KeyTransRecipientId(x500Name, x509Cert.getSerialNumber());
RecipientInformationStore recipientInfoStore = envelope.getRecipientInfos();

Collection<RecipientInformation> recipients = recipientInfoStore.getRecipients();

if (recipients == null) {
throw new GeneralSecurityException("Certificate recipients could not be extracted from the inbound message envelope.");
}
//RecipientInformation recipientInfo = recipientInfoStore.get(recId);
//Object recipient = null;

boolean foundRecipient = false;
for (Iterator<RecipientInformation> iterator = recipients.iterator(); iterator.hasNext(); ) {
RecipientInformation recipientInfo = iterator.next();
//recipient = iterator.next();
if (recipientInfo instanceof KeyTransRecipientInformation) {
// X509CertificateHolder x509CertHolder = new X509CertificateHolder(x509Cert.getEncoded());
for (RecipientInformation recipientInfo : recipients) {

//RecipientId rid = recipientInfo.getRID();
if(logger.isDebugEnabled()) {
logger.debug("Recipient RID: " + recipientInfo.getRID());
logger.debug("Recipient Info Type: " + recipientInfo.getClass().getName());
logger.debug("Using private key algorithm: " + receiverKey.getAlgorithm());
}

if (recipientInfo instanceof KeyTransRecipientInformation) {
KeyTransRecipientId certRecId = new KeyTransRecipientId(x500Name, x509Cert.getSerialNumber());
if (certRecId.match(recipientInfo) && !foundRecipient) {
foundRecipient = true;
// byte[] decryptedData = recipientInfo.getContent(new JceKeyTransEnvelopedRecipient((PrivateKey)key).setProvider("BC"));
byte[] decryptedData = recipientInfo.getContent(new BcRSAKeyTransEnvelopedRecipient(PrivateKeyFactory.createKey(PrivateKeyInfo.getInstance(key.getEncoded()))));
byte[] decryptedData = recipientInfo.getContent(
new BcRSAKeyTransEnvelopedRecipient(PrivateKeyFactory.createKey(
PrivateKeyInfo.getInstance(receiverKey.getEncoded())))
);
return SMIMEUtil.toMimeBodyPart(decryptedData);
}

} else if (recipientInfo instanceof KeyAgreeRecipientInformation) {
KeyAgreeRecipientId certRecId = new KeyAgreeRecipientId(x500Name, x509Cert.getSerialNumber());
if (certRecId.match(recipientInfo) && !foundRecipient) {
foundRecipient = true;
byte[] decryptedData = recipientInfo.getContent(
new JceKeyAgreeEnvelopedRecipient((PrivateKey) receiverKey).setProvider("BC")
);
return SMIMEUtil.toMimeBodyPart(decryptedData);
} else {
if (logger.isDebugEnabled()) {
logger.debug("Failed match on recipient ID's:: RID type from msg:" + recipientInfo.getRID().getType() + " RID type from priv cert: " + certRecId.getType());
}
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("Failed match on recipient ID's:: RID type from msg: "
+ recipientInfo.getRID().getType()
+ " RID type from priv cert: "
+ (recipientInfo instanceof KeyTransRecipientInformation ? "0" : "2"));
}
}
}
throw new GeneralSecurityException(
"Matching certificate recipient could not be found trying to decrypt the message."
+ " Either the sender has encrypted the message with a public key that does not match"
+ " a private key in your keystore or the there is a problem in your keystore where the private key has not been imported or is corrupt.");

if (!foundRecipient) {
throw new GeneralSecurityException(
"Matching certificate recipient could not be found trying to decrypt the message."
+ " Either the sender has encrypted the message with a public key that does not match"
+ " a private key in your keystore or there is a problem in your keystore where the private key has not been imported or is corrupt.");
}

return null;
}


public void deinitialize() {
}

public MimeBodyPart encrypt(MimeBodyPart part, Certificate cert, String algorithm, String contentTxfrEncoding) throws GeneralSecurityException, SMIMEException, MessagingException {
X509Certificate x509Cert = castCertificate(cert);

public MimeBodyPart encrypt(MimeBodyPart part, Certificate receiverCert, String encryptionAlgorithm, String contentTxfrEncoding) throws Exception {
X509Certificate x509Cert = castCertificate(receiverCert);

SMIMEEnvelopedGenerator gen = new SMIMEEnvelopedGenerator();
gen.setContentTransferEncoding(getEncoding(contentTxfrEncoding));

String receiverCertAlgorithm = receiverCert.getPublicKey().getAlgorithm();
boolean isECKey = "EC".equalsIgnoreCase(receiverCertAlgorithm);
OutputEncryptor encryptor = null;

if (logger.isDebugEnabled()) {
logger.debug("Encrypting on MIME part containing the following headers: " + AS2Util.printHeaders(part.getAllHeaders()));
logger.debug("Receiver Cert Algorithm: " + receiverCertAlgorithm);
logger.debug("Encryption Algorithm from Partnership: " + encryptionAlgorithm);
}

gen.addRecipientInfoGenerator(new JceKeyTransRecipientInfoGenerator(x509Cert).setProvider("BC"));
if ("RSA".equalsIgnoreCase(receiverCertAlgorithm)) {
logger.debug("Using RSA based encryption...");
gen.addRecipientInfoGenerator(
new JceKeyTransRecipientInfoGenerator(x509Cert).setProvider("BC")
);
encryptor = getOutputEncryptor(encryptionAlgorithm, isECKey, false);

} else if ("EC".equalsIgnoreCase(receiverCertAlgorithm)) {
logger.debug("Using EC based encryption...");

KeyPair keypair = this.generateECKeypair(receiverCert);

JceKeyAgreeRecipientInfoGenerator recipientGenerator
= new JceKeyAgreeRecipientInfoGenerator(
CMSAlgorithm.ECMQV_SHA256KDF,
keypair.getPrivate(),
keypair.getPublic(),
getEncryptionOID(encryptionAlgorithm, isECKey, true)
).setProvider("BC");

recipientGenerator.addRecipient((X509Certificate) receiverCert);

encryptor = getOutputEncryptor(encryptionAlgorithm, isECKey, false);

return gen.generate(part, getOutputEncryptor(algorithm));
gen.addRecipientInfoGenerator(recipientGenerator);

} else {
throw new GeneralSecurityException("Unsupported certificate algorithm: " + receiverCertAlgorithm);
}

return gen.generate(part, encryptor);
}

/**
* Generates a key pair on a special EC
*/
private KeyPair generateECKeypair(Certificate cert) throws Exception {
if (!cert.getPublicKey().getAlgorithm().equals("EC")) {
throw new IllegalArgumentException("BCCryptoHelper.generateECKeypair requires a certificate where the public keys algorithm is \"EC\"");
}
ECPublicKey ecPublicKey = (ECPublicKey) cert.getPublicKey();
ECParameterSpec ecSpec = ecPublicKey.getParams();
KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("ECDSA", "BC");
keyGenerator.initialize(ecSpec, new SecureRandom());
KeyPair keyPair = keyGenerator.generateKeyPair();
return (keyPair);
}

public void initialize() {
Expand Down Expand Up @@ -577,58 +644,118 @@ public String convertAlgorithm(String algorithm, boolean toBC) throws NoSuchAlgo

}


/**
* Looks up the correct ASN1 OID of the passed in algorithm string and returns the encryptor.
* The encryption key length is set where necessary
* Resolves the appropriate ASN1 OID for the given encryption algorithm.
* This method determines the correct object identifier to use for encryption
* based on the algorithm name, whether the encryption is being used with
* EC keys, and whether the algorithm should support wrapping.
*
* @param algorithm The name of the algorithm to use for encryption
* @return the OutputEncryptor of the given hash algorithm
* @throws NoSuchAlgorithmException - Houston we have a problem
* <p>
* TODO: Possibly just use new ASN1ObjectIdentifier(algorithm) instead of explicit lookup to support random configured algorithms
* but will require determining if this has any side effects from a security point of view.
*/
protected OutputEncryptor getOutputEncryptor(String algorithm) throws NoSuchAlgorithmException {
* @param algorithm The name of the encryption algorithm (e.g., "AES128_CBC").
* @param isECKey Indicates if the encryption involves EC (Elliptic Curve) keys.
* @param isWrap Indicates if the algorithm is being used for key wrapping.
* @return The ASN1ObjectIdentifier representing the OID for the encryption algorithm.
* @throws NoSuchAlgorithmException If the algorithm is null, unsupported,
* or incompatible with the given parameters.
*
* <p><b>Behavior:</b></p>
* <ul>
* <li>Throws an exception for unsupported or invalid algorithm names.</li>
* <li>Handles special cases for EC keys, ensuring compatibility with
* wrapping and non-wrapping modes.</li>
* <li>Supports a variety of algorithms, including AES, 3DES, RC2, CAST5,
* and IDEA, with appropriate restrictions for EC key usage.</li>
* </ul>
*/
protected ASN1ObjectIdentifier getEncryptionOID(String algorithm, boolean isECKey, boolean isWrap) throws NoSuchAlgorithmException {
if (algorithm == null) {
throw new NoSuchAlgorithmException("Algorithm is null");
}
ASN1ObjectIdentifier asn1ObjId = null;
int keyLen = -1;

if (algorithm.equalsIgnoreCase(DIGEST_MD2)) {
asn1ObjId = new ASN1ObjectIdentifier(PKCSObjectIdentifiers.md2.getId());
return new ASN1ObjectIdentifier(PKCSObjectIdentifiers.md2.getId());
} else if (algorithm.equalsIgnoreCase(DIGEST_MD5)) {
asn1ObjId = new ASN1ObjectIdentifier(PKCSObjectIdentifiers.md5.getId());
return new ASN1ObjectIdentifier(PKCSObjectIdentifiers.md5.getId());
} else if (algorithm.equalsIgnoreCase(DIGEST_SHA1)) {
asn1ObjId = new ASN1ObjectIdentifier(OIWObjectIdentifiers.idSHA1.getId());
return new ASN1ObjectIdentifier(OIWObjectIdentifiers.idSHA1.getId());
} else if (algorithm.equalsIgnoreCase(DIGEST_SHA224)) {
asn1ObjId = new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha224.getId());
return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha224.getId());
} else if (algorithm.equalsIgnoreCase(DIGEST_SHA256)) {
asn1ObjId = new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha256.getId());
return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha256.getId());
} else if (algorithm.equalsIgnoreCase(DIGEST_SHA384)) {
asn1ObjId = new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha384.getId());
return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha384.getId());
} else if (algorithm.equalsIgnoreCase(DIGEST_SHA512)) {
asn1ObjId = new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha512.getId());
return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha512.getId());
} else if (algorithm.equalsIgnoreCase(CRYPT_3DES)) {
asn1ObjId = new ASN1ObjectIdentifier(PKCSObjectIdentifiers.des_EDE3_CBC.getId());
if (isECKey) {
if (isWrap) {
return CMSAlgorithm.DES_EDE3_WRAP;
} else {
return CMSAlgorithm.DES_EDE3_CBC;
}
} else {
return new ASN1ObjectIdentifier(PKCSObjectIdentifiers.des_EDE3_CBC.getId());
}
} else if (algorithm.equalsIgnoreCase(CRYPT_RC2) || algorithm.equalsIgnoreCase(CRYPT_RC2_CBC)) {
asn1ObjId = new ASN1ObjectIdentifier(PKCSObjectIdentifiers.RC2_CBC.getId());
keyLen = 40;
if (isECKey) {
if (isWrap) {
throw new NoSuchAlgorithmException("RC2 Wrap is not supported for EC keys");
}
}
return new ASN1ObjectIdentifier(PKCSObjectIdentifiers.RC2_CBC.getId());
} else if (algorithm.equalsIgnoreCase(AES128_CBC)) {
asn1ObjId = CMSAlgorithm.AES128_CBC;
if (isECKey && isWrap) {
return CMSAlgorithm.AES128_WRAP;
}
return CMSAlgorithm.AES128_CBC;
} else if (algorithm.equalsIgnoreCase(AES192_CBC)) {
asn1ObjId = CMSAlgorithm.AES192_CBC;
if (isECKey && isWrap) {
return CMSAlgorithm.AES192_WRAP;
}
return CMSAlgorithm.AES192_CBC;
} else if (algorithm.equalsIgnoreCase(AES256_CBC)) {
asn1ObjId = CMSAlgorithm.AES256_CBC;
if (isECKey && isWrap) {
return CMSAlgorithm.AES256_WRAP;
}
return CMSAlgorithm.AES256_CBC;
} else if (algorithm.equalsIgnoreCase(AES256_WRAP)) {
asn1ObjId = CMSAlgorithm.AES256_WRAP;
return CMSAlgorithm.AES256_WRAP;
} else if (algorithm.equalsIgnoreCase(CRYPT_CAST5)) {
asn1ObjId = CMSAlgorithm.CAST5_CBC;
if (isECKey) {
throw new NoSuchAlgorithmException("CAST5 is not supported for EC keys");
}
return CMSAlgorithm.CAST5_CBC;
} else if (algorithm.equalsIgnoreCase(CRYPT_IDEA)) {
asn1ObjId = CMSAlgorithm.IDEA_CBC;
if (isECKey) {
throw new NoSuchAlgorithmException("IDEA is not supported for EC keys");
}
return CMSAlgorithm.IDEA_CBC;
} else {
throw new NoSuchAlgorithmException("Unsupported or invalid algorithm: " + algorithm);
}
}

/**
* Looks up the correct ASN1 OID of the passed in algorithm string and returns the encryptor.
* The encryption key length is set where necessary
*
* @param algorithm The name of the algorithm to use for encryption
* @return the OutputEncryptor of the given hash algorithm
* @throws NoSuchAlgorithmException - Houston we have a problem
* <p>
* TODO: Possibly just use new ASN1ObjectIdentifier(algorithm) instead of explicit lookup to support random configured algorithms
* but will require determining if this has any side effects from a security point of view.
*/
protected OutputEncryptor getOutputEncryptor(String algorithm, boolean isECKey, boolean isWrap) throws NoSuchAlgorithmException {
if (algorithm == null) {
throw new NoSuchAlgorithmException("Algorithm is null");
}
ASN1ObjectIdentifier asn1ObjId = getEncryptionOID(algorithm, isECKey, isWrap);
int keyLen = -1;

if (algorithm.equalsIgnoreCase(CRYPT_RC2) || algorithm.equalsIgnoreCase(CRYPT_RC2_CBC)) {
keyLen = 40;
}

OutputEncryptor oe = null;
try {
if (keyLen < 0) {
Expand Down
Binary file not shown.
Binary file not shown.