
com.helger.as2lib.crypto.BCCryptoHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of as2-lib Show documentation
Show all versions of as2-lib Show documentation
Open AS2 fork - library part
/**
* The FreeBSD Copyright
* Copyright 1994-2008 The FreeBSD Project. All rights reserved.
* Copyright (C) 2013-2016 Philip Helger philip[at]helger[dot]com
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. 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.
*
* THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``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 FREEBSD PROJECT 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.
*
* The views and conclusions contained in the software and documentation
* are those of the authors and should not be interpreted as representing
* official policies, either expressed or implied, of the FreeBSD Project.
*/
package com.helger.as2lib.crypto;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.DigestOutputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.Locale;
import javax.activation.CommandMap;
import javax.activation.MailcapCommandMap;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.mail.MessagingException;
import javax.mail.internet.ContentType;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.smime.SMIMECapabilitiesAttribute;
import org.bouncycastle.asn1.smime.SMIMECapabilityVector;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.RecipientId;
import org.bouncycastle.cms.RecipientInformation;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationVerifier;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder;
import org.bouncycastle.cms.jcajce.JceKeyTransEnvelopedRecipient;
import org.bouncycastle.cms.jcajce.JceKeyTransRecipientId;
import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.mail.smime.SMIMEEnveloped;
import org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator;
import org.bouncycastle.mail.smime.SMIMEException;
import org.bouncycastle.mail.smime.SMIMESignedGenerator;
import org.bouncycastle.mail.smime.SMIMESignedParser;
import org.bouncycastle.mail.smime.SMIMEUtil;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.OutputEncryptor;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.helger.as2lib.exception.OpenAS2Exception;
import com.helger.as2lib.exception.WrappedOpenAS2Exception;
import com.helger.as2lib.util.CAS2Header;
import com.helger.as2lib.util.IOHelper;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.base64.Base64;
import com.helger.commons.collection.CollectionHelper;
import com.helger.commons.collection.ext.CommonsArrayList;
import com.helger.commons.collection.ext.ICommonsList;
import com.helger.commons.io.file.FileHelper;
import com.helger.commons.io.stream.NullOutputStream;
import com.helger.commons.io.stream.StreamHelper;
import com.helger.commons.lang.priviledged.AccessControllerHelper;
import com.helger.commons.string.StringHelper;
import com.helger.commons.system.SystemProperties;
import com.helger.mail.cte.EContentTransferEncoding;
/**
* Implementation of {@link ICryptoHelper} based on BouncyCastle.
*
* @author Philip Helger
*/
public final class BCCryptoHelper implements ICryptoHelper
{
private static final Logger s_aLogger = LoggerFactory.getLogger (BCCryptoHelper.class);
private static final File s_aDumpDecryptedDirectory;
static
{
final String sDumpDecryptedDirectory = SystemProperties.getPropertyValueOrNull ("AS2.dumpDecryptedDirectory");
if (StringHelper.hasText (sDumpDecryptedDirectory))
{
s_aDumpDecryptedDirectory = new File (sDumpDecryptedDirectory);
IOHelper.getFileOperationManager ().createDirIfNotExisting (s_aDumpDecryptedDirectory);
s_aLogger.info ("Using directory " +
s_aDumpDecryptedDirectory.getAbsolutePath () +
" to dump all decrypted body parts to.");
}
else
s_aDumpDecryptedDirectory = null;
}
public BCCryptoHelper ()
{
Security.addProvider (new BouncyCastleProvider ());
final MailcapCommandMap aCommandMap = (MailcapCommandMap) CommandMap.getDefaultCommandMap ();
aCommandMap.addMailcap ("application/pkcs7-signature;; x-java-content-handler=" +
org.bouncycastle.mail.smime.handlers.pkcs7_signature.class.getName ());
aCommandMap.addMailcap ("application/pkcs7-mime;; x-java-content-handler=" +
org.bouncycastle.mail.smime.handlers.pkcs7_mime.class.getName ());
aCommandMap.addMailcap ("application/x-pkcs7-signature;; x-java-content-handler=" +
org.bouncycastle.mail.smime.handlers.x_pkcs7_signature.class.getName ());
aCommandMap.addMailcap ("application/x-pkcs7-mime;; x-java-content-handler=" +
org.bouncycastle.mail.smime.handlers.x_pkcs7_mime.class.getName ());
aCommandMap.addMailcap ("multipart/signed;; x-java-content-handler=" +
org.bouncycastle.mail.smime.handlers.multipart_signed.class.getName ());
AccessControllerHelper.run ( () -> {
CommandMap.setDefaultCommandMap (aCommandMap);
return null;
});
}
@Nonnull
public KeyStore createNewKeyStore () throws KeyStoreException, NoSuchProviderException
{
return KeyStore.getInstance ("PKCS12", BouncyCastleProvider.PROVIDER_NAME);
}
@Nonnull
public KeyStore loadKeyStore (@Nullable final InputStream aIS, @Nonnull final char [] aPassword) throws Exception
{
final KeyStore aKeyStore = createNewKeyStore ();
if (aIS != null)
aKeyStore.load (aIS, aPassword);
return aKeyStore;
}
@Nonnull
@Deprecated
public KeyStore loadKeyStore (@Nonnull final String sFilename, @Nonnull final char [] aPassword) throws Exception
{
final InputStream aIS = FileHelper.getInputStream (sFilename);
try
{
return loadKeyStore (aIS, aPassword);
}
finally
{
StreamHelper.close (aIS);
}
}
public boolean isEncrypted (@Nonnull final MimeBodyPart aPart) throws MessagingException
{
ValueEnforcer.notNull (aPart, "Part");
// Content-Type is sthg like this if encrypted:
// application/pkcs7-mime; name=smime.p7m; smime-type=enveloped-data
final ContentType aContentType = new ContentType (aPart.getContentType ());
final String sBaseType = aContentType.getBaseType ().toLowerCase (Locale.US);
if (!sBaseType.equals ("application/pkcs7-mime"))
return false;
final String sSmimeType = aContentType.getParameter ("smime-type");
return sSmimeType != null && sSmimeType.equalsIgnoreCase ("enveloped-data");
}
public boolean isSigned (@Nonnull final MimeBodyPart aPart) throws MessagingException
{
ValueEnforcer.notNull (aPart, "Part");
final ContentType aContentType = new ContentType (aPart.getContentType ());
final String sBaseType = aContentType.getBaseType ();
return sBaseType.equalsIgnoreCase ("multipart/signed");
}
public boolean isCompressed (@Nonnull final String sContentType) throws OpenAS2Exception
{
ValueEnforcer.notNull (sContentType, "ContentType");
try
{
// Content-Type is sthg like this if compressed:
// application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z
final ContentType aContentType = new ContentType (sContentType);
final String sSmimeType = aContentType.getParameter ("smime-type");
return sSmimeType != null && sSmimeType.equalsIgnoreCase ("compressed-data");
}
catch (final MessagingException ex)
{
throw WrappedOpenAS2Exception.wrap (ex);
}
}
@Nonnull
@ReturnsMutableCopy
private static byte [] _getAsciiBytes (@Nonnull final String sString)
{
final char [] aChars = sString.toCharArray ();
final int nLength = aChars.length;
final byte [] ret = new byte [nLength];
for (int i = 0; i < nLength; i++)
ret[i] = (byte) aChars[i];
return ret;
}
@Nonnull
public String calculateMIC (@Nonnull final MimeBodyPart aPart,
@Nonnull final ECryptoAlgorithmSign eDigestAlgorithm,
final boolean bIncludeHeaders) throws GeneralSecurityException,
MessagingException,
IOException
{
ValueEnforcer.notNull (aPart, "MimeBodyPart");
ValueEnforcer.notNull (eDigestAlgorithm, "DigestAlgorithm");
if (s_aLogger.isDebugEnabled ())
s_aLogger.debug ("BCCryptoHelper.calculateMIC (" +
eDigestAlgorithm +
" [" +
eDigestAlgorithm.getOID ().getId () +
"], " +
bIncludeHeaders +
")");
final ASN1ObjectIdentifier aMICAlg = eDigestAlgorithm.getOID ();
final MessageDigest aMessageDigest = MessageDigest.getInstance (aMICAlg.getId (),
BouncyCastleProvider.PROVIDER_NAME);
if (bIncludeHeaders)
{
// Start hashing the header
final byte [] aCRLF = new byte [] { '\r', '\n' };
final Enumeration > aHeaderLines = aPart.getAllHeaderLines ();
while (aHeaderLines.hasMoreElements ())
{
aMessageDigest.update (_getAsciiBytes ((String) aHeaderLines.nextElement ()));
aMessageDigest.update (aCRLF);
}
// The CRLF separator between header and content
aMessageDigest.update (aCRLF);
}
// No need to canonicalize here - see issue #12
try (final DigestOutputStream aDOS = new DigestOutputStream (new NullOutputStream (), aMessageDigest);
final OutputStream aOS = MimeUtility.encode (aDOS, aPart.getEncoding ()))
{
aPart.getDataHandler ().writeTo (aOS);
}
// Build result digest array
final byte [] aMIC = aMessageDigest.digest ();
// Perform Base64 encoding and append algorithm ID
final String ret = Base64.encodeBytes (aMIC) + ", " + eDigestAlgorithm.getID ();
if (s_aLogger.isDebugEnabled ())
s_aLogger.debug (" MIC = " + ret);
return ret;
}
private static void _dumpDecrypted (@Nonnull final byte [] aPayload)
{
// Ensure a unique filename
File aDestinationFile;
int nIndex = 0;
do
{
aDestinationFile = new File (s_aDumpDecryptedDirectory,
"as2-decrypted-" + Long.toString (new Date ().getTime ()) + "-" + nIndex + ".part");
nIndex++;
} while (aDestinationFile.exists ());
s_aLogger.info ("Dumping decrypted MIME part to file " + aDestinationFile.getAbsolutePath ());
final OutputStream aOS = FileHelper.getOutputStream (aDestinationFile);
try
{
// Add payload
aOS.write (aPayload);
}
catch (final IOException ex)
{
s_aLogger.error ("Failed to dump decrypted MIME part to file " + aDestinationFile.getAbsolutePath (), ex);
}
finally
{
StreamHelper.close (aOS);
}
}
@Nonnull
public MimeBodyPart decrypt (@Nonnull final MimeBodyPart aPart,
@Nonnull final X509Certificate aX509Cert,
@Nonnull final PrivateKey aPrivateKey,
final boolean bForceDecrypt) throws GeneralSecurityException,
MessagingException,
CMSException,
SMIMEException
{
ValueEnforcer.notNull (aPart, "MimeBodyPart");
ValueEnforcer.notNull (aX509Cert, "X509Cert");
ValueEnforcer.notNull (aPrivateKey, "PrivateKey");
if (s_aLogger.isDebugEnabled ())
s_aLogger.debug ("BCCryptoHelper.decrypt; X509 subject=" +
aX509Cert.getSubjectX500Principal ().getName () +
"; forceDecrypt=" +
bForceDecrypt);
// Make sure the data is encrypted
if (!bForceDecrypt && !isEncrypted (aPart))
throw new GeneralSecurityException ("Content-Type indicates data isn't encrypted: " + aPart.getContentType ());
// Parse the MIME body into an SMIME envelope object
final SMIMEEnveloped aEnvelope = new SMIMEEnveloped (aPart);
// Get the recipient object for decryption
final RecipientId aRecipientID = new JceKeyTransRecipientId (aX509Cert);
final RecipientInformation aRecipient = aEnvelope.getRecipientInfos ().get (aRecipientID);
if (aRecipient == null)
throw new GeneralSecurityException ("Certificate does not match part signature");
// try to decrypt the data
final byte [] aDecryptedData = aRecipient.getContent (new JceKeyTransEnvelopedRecipient (aPrivateKey).setProvider (BouncyCastleProvider.PROVIDER_NAME));
if (s_aDumpDecryptedDirectory != null)
_dumpDecrypted (aDecryptedData);
return SMIMEUtil.toMimeBodyPart (aDecryptedData);
}
@Nonnull
public MimeBodyPart encrypt (@Nonnull final MimeBodyPart aPart,
@Nonnull final X509Certificate aX509Cert,
@Nonnull final ECryptoAlgorithmCrypt eAlgorithm) throws GeneralSecurityException,
SMIMEException,
CMSException
{
ValueEnforcer.notNull (aPart, "MimeBodyPart");
ValueEnforcer.notNull (aX509Cert, "X509Cert");
ValueEnforcer.notNull (eAlgorithm, "Algorithm");
if (s_aLogger.isDebugEnabled ())
s_aLogger.debug ("BCCryptoHelper.encrypt; X509 subject=" +
aX509Cert.getSubjectX500Principal ().getName () +
"; algorithm=" +
eAlgorithm);
// Check if the certificate is expired or active.
aX509Cert.checkValidity ();
final ASN1ObjectIdentifier aEncAlg = eAlgorithm.getOID ();
final SMIMEEnvelopedGenerator aGen = new SMIMEEnvelopedGenerator ();
aGen.addRecipientInfoGenerator (new JceKeyTransRecipientInfoGenerator (aX509Cert).setProvider (BouncyCastleProvider.PROVIDER_NAME));
final OutputEncryptor aEncryptor = new JceCMSContentEncryptorBuilder (aEncAlg).setProvider (BouncyCastleProvider.PROVIDER_NAME)
.build ();
final MimeBodyPart aEncData = aGen.generate (aPart, aEncryptor);
return aEncData;
}
@Nonnull
public MimeBodyPart sign (@Nonnull final MimeBodyPart aPart,
@Nonnull final X509Certificate aX509Cert,
@Nonnull final PrivateKey aPrivateKey,
@Nonnull final ECryptoAlgorithmSign eAlgorithm,
final boolean bIncludeCertificateInSignedContent,
final boolean bUseOldRFC3851MicAlgs) throws GeneralSecurityException,
SMIMEException,
MessagingException,
OperatorCreationException
{
ValueEnforcer.notNull (aPart, "MimeBodyPart");
ValueEnforcer.notNull (aX509Cert, "X509Cert");
ValueEnforcer.notNull (aPrivateKey, "PrivateKey");
ValueEnforcer.notNull (eAlgorithm, "Algorithm");
if (s_aLogger.isDebugEnabled ())
s_aLogger.debug ("BCCryptoHelper.sign; X509 subject=" +
aX509Cert.getSubjectX500Principal ().getName () +
"; algorithm=" +
eAlgorithm +
"; includeCertificateInSignedContent=" +
bIncludeCertificateInSignedContent);
// Check if the certificate is expired or active.
aX509Cert.checkValidity ();
// create a CertStore containing the certificates we want carried
// in the signature
final ICommonsList aCertList = new CommonsArrayList<> (aX509Cert);
final JcaCertStore aCertStore = new JcaCertStore (aCertList);
// create some smime capabilities in case someone wants to respond
final ASN1EncodableVector aSignedAttrs = new ASN1EncodableVector ();
final SMIMECapabilityVector aCapabilities = new SMIMECapabilityVector ();
aCapabilities.addCapability (eAlgorithm.getOID ());
aSignedAttrs.add (new SMIMECapabilitiesAttribute (aCapabilities));
// add an encryption key preference for encrypted responses -
// normally this would be different from the signing certificate...
// final IssuerAndSerialNumber issAndSer = new IssuerAndSerialNumber (new
// X500Name (signDN),
// aX509Cert.getSerialNumber ());
// aSignedAttrs.add (new SMIMEEncryptionKeyPreferenceAttribute (issAndSer));
// create the generator for creating an smime/signed message
final SMIMESignedGenerator aSGen = new SMIMESignedGenerator (bUseOldRFC3851MicAlgs ? SMIMESignedGenerator.RFC3851_MICALGS
: SMIMESignedGenerator.RFC5751_MICALGS);
// aSGen.addSigner (aPrivKey, aX509Cert, aSignDigest.getId ());
// add a signer to the generator - this specifies we are using SHA1 and
// adding the smime attributes above to the signed attributes that
// will be generated as part of the signature. The encryption algorithm
// used is taken from the key - in this RSA with PKCS1Padding
aSGen.addSignerInfoGenerator (new JcaSimpleSignerInfoGeneratorBuilder ().setProvider (BouncyCastleProvider.PROVIDER_NAME)
.setSignedAttributeGenerator (new AttributeTable (aSignedAttrs))
.build (eAlgorithm.getSignAlgorithmName (),
aPrivateKey,
aX509Cert));
if (bIncludeCertificateInSignedContent)
{
// add our pool of certs and cerls (if any) to go with the signature
aSGen.addCertificates (aCertStore);
}
final MimeMultipart aSignedData = aSGen.generate (aPart);
final MimeBodyPart aTmpBody = new MimeBodyPart ();
aTmpBody.setContent (aSignedData);
aTmpBody.setHeader (CAS2Header.HEADER_CONTENT_TYPE, aSignedData.getContentType ());
return aTmpBody;
}
@Nonnull
private X509Certificate _verifyFindCertificate (@Nullable final X509Certificate aX509Cert,
final boolean bUseCertificateInBodyPart,
@Nonnull final SMIMESignedParser aSignedParser) throws CMSException,
CertificateException,
GeneralSecurityException
{
X509Certificate aRealX509Cert = aX509Cert;
if (bUseCertificateInBodyPart)
{
// get all certificates contained in the body part
final Collection > aContainedCerts = aSignedParser.getCertificates ().getMatches (null);
if (!aContainedCerts.isEmpty ())
{
// For PEPPOL the certificate is passed in
if (aContainedCerts.size () > 1)
s_aLogger.warn ("Signed part contains " + aContainedCerts.size () + " certificates - using the first one!");
final X509CertificateHolder aCertHolder = ((X509CertificateHolder) CollectionHelper.getFirstElement (aContainedCerts));
final X509Certificate aCert = new JcaX509CertificateConverter ().setProvider (BouncyCastleProvider.PROVIDER_NAME)
.getCertificate (aCertHolder);
if (aX509Cert != null && !aX509Cert.equals (aCert))
s_aLogger.warn ("Certificate mismatch! Provided certificate\n" +
aX509Cert +
" differs from certficate contained in message\n" +
aCert);
aRealX509Cert = aCert;
}
}
if (aRealX509Cert == null)
throw new GeneralSecurityException ("No certificate provided" +
(bUseCertificateInBodyPart ? " and none found in the message" : "") +
"!");
return aRealX509Cert;
}
@Nonnull
public MimeBodyPart verify (@Nonnull final MimeBodyPart aPart,
@Nullable final X509Certificate aX509Cert,
final boolean bUseCertificateInBodyPart,
final boolean bForceVerify) throws GeneralSecurityException,
IOException,
MessagingException,
CMSException,
OperatorCreationException
{
if (s_aLogger.isDebugEnabled ())
s_aLogger.debug ("BCCryptoHelper.verify; X509 subject=" +
(aX509Cert == null ? "null" : aX509Cert.getSubjectX500Principal ().getName ()) +
"; useCertificateInBodyPart=" +
bUseCertificateInBodyPart +
"; forceVerify=" +
bForceVerify);
// Make sure the data is signed
if (!bForceVerify && !isSigned (aPart))
throw new GeneralSecurityException ("Content-Type indicates data isn't signed: " + aPart.getContentType ());
final MimeMultipart aMainPart = (MimeMultipart) aPart.getContent ();
// SMIMESignedParser uses "7bit" as the default - AS2 wants "binary"
final SMIMESignedParser aSignedParser = new SMIMESignedParser (new JcaDigestCalculatorProviderBuilder ().setProvider (BouncyCastleProvider.PROVIDER_NAME)
.build (),
aMainPart,
EContentTransferEncoding.AS2_DEFAULT.getID ());
final X509Certificate aRealX509Cert = _verifyFindCertificate (aX509Cert, bUseCertificateInBodyPart, aSignedParser);
if (s_aLogger.isDebugEnabled ())
s_aLogger.debug (aRealX509Cert == aX509Cert ? "Verifying signature using the provided certificate (partnership)"
: "Verifying signature using the certificate contained in the MIME body part");
// Check if the certificate is expired or active.
aRealX509Cert.checkValidity ();
// Verify certificate
final SignerInformationVerifier aSIV = new JcaSimpleSignerInfoVerifierBuilder ().setProvider (BouncyCastleProvider.PROVIDER_NAME)
.build (aRealX509Cert.getPublicKey ());
for (final Object aSigner : aSignedParser.getSignerInfos ().getSigners ())
{
final SignerInformation aSignerInfo = (SignerInformation) aSigner;
if (!aSignerInfo.verify (aSIV))
throw new SignatureException ("Verification failed");
}
return aSignedParser.getContent ();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy