Introduzione a BouncyCastle con Java

1. Panoramica

BouncyCastle è una libreria Java che integra la Java Cryptographic Extension (JCE) predefinita.

In questo articolo introduttivo, mostreremo come utilizzare BouncyCastle per eseguire operazioni crittografiche, come crittografia e firma.

2. Configurazione Maven

Prima di iniziare a lavorare con la libreria, dobbiamo aggiungere le dipendenze richieste al nostro file pom.xml :

 org.bouncycastle bcpkix-jdk15on 1.58 

Tieni presente che possiamo sempre cercare le ultime versioni delle dipendenze nel Maven Central Repository.

3. Imposta file di criteri di giurisdizione di forza illimitata

L'installazione standard di Java è limitata in termini di forza per le funzioni crittografiche, ciò è dovuto a politiche che vietano l'uso di una chiave con una dimensione che supera determinati valori, ad esempio 128 per AES.

Per superare questa limitazione, è necessario configurare i file dei criteri di giurisdizione con forza illimitata .

Per fare ciò, dobbiamo prima scaricare il pacchetto seguendo questo link. Successivamente, dobbiamo estrarre il file zippato in una directory di nostra scelta, che contiene due file jar:

  • local_policy.jar
  • US_export_policy.jar

Infine, dobbiamo cercare la cartella {JAVA_HOME} / lib / security e sostituire i file dei criteri esistenti con quelli che abbiamo estratto qui.

Tieni presente che in Java 9 non è più necessario scaricare il pacchetto dei file dei criteri , è sufficiente impostare la proprietà crypto.policy su unlimited :

Security.setProperty("crypto.policy", "unlimited");

Una volta fatto, dobbiamo verificare che la configurazione funzioni correttamente:

int maxKeySize = javax.crypto.Cipher.getMaxAllowedKeyLength("AES"); System.out.println("Max Key Size for AES : " + maxKeySize);

Di conseguenza:

Max Key Size for AES : 2147483647

In base alla dimensione massima della chiave restituita dal metodo getMaxAllowedKeyLength () , possiamo tranquillamente affermare che i file dei criteri di forza illimitata sono stati installati correttamente.

Se il valore restituito è uguale a 128, dobbiamo assicurarci di aver installato i file nella JVM in cui stiamo eseguendo il codice.

4. Operazioni crittografiche

4.1. Preparazione del certificato e della chiave privata

Prima di passare all'implementazione delle funzioni crittografiche, dobbiamo prima creare un certificato e una chiave privata.

A scopo di test, possiamo utilizzare queste risorse:

  • Baeldung.cer
  • Baeldung.p12 (password = "password")

Baeldung.cer è un certificato digitale che utilizza lo standard internazionale di infrastruttura a chiave pubblica X.509, mentre Baeldung.p12 è un keystore PKCS12 protetto da password che contiene una chiave privata.

Vediamo come questi possono essere caricati in Java:

Security.addProvider(new BouncyCastleProvider()); CertificateFactory certFactory= CertificateFactory .getInstance("X.509", "BC"); X509Certificate certificate = (X509Certificate) certFactory .generateCertificate(new FileInputStream("Baeldung.cer")); char[] keystorePassword = "password".toCharArray(); char[] keyPassword = "password".toCharArray(); KeyStore keystore = KeyStore.getInstance("PKCS12"); keystore.load(new FileInputStream("Baeldung.p12"), keystorePassword); PrivateKey key = (PrivateKey) keystore.getKey("baeldung", keyPassword);

Innanzitutto, abbiamo aggiunto BouncyCastleProvider come provider di sicurezza in modo dinamico utilizzando il metodo addProvider () .

Questo può essere fatto anche staticamente modificando il file {JAVA_HOME} /jre/lib/security/java.security e aggiungendo questa riga:

security.provider.N = org.bouncycastle.jce.provider.BouncyCastleProvider

Una volta installato correttamente il provider, abbiamo creato un oggetto CertificateFactory utilizzando il metodo getInstance () .

Il metodo getInstance () accetta due argomenti; il tipo di certificato "X.509" e il provider di sicurezza "BC".

L' istanza certFactory viene successivamente utilizzata per generare un oggetto X509Certificate , tramite il metodo generateCertificate () .

Allo stesso modo, abbiamo creato un oggetto Keystore PKCS12, su cui viene chiamato il metodo load () .

Il metodo getKey () restituisce la chiave privata associata a un dato alias.

Si noti che un keystore PKCS12 contiene un set di chiavi private, ogni chiave privata può avere una password specifica, ecco perché abbiamo bisogno di una password globale per aprire il keystore e di una specifica per recuperare la chiave privata.

Il Certificato e la coppia di chiavi private vengono utilizzati principalmente nelle operazioni di crittografia asimmetrica:

  • Crittografia
  • Decrittazione
  • Firma
  • Verifica

4.2 Crittografia e decrittografia CMS / PKCS7

Nella crittografia con crittografia asimmetrica, ogni comunicazione richiede un certificato pubblico e una chiave privata.

Il destinatario è vincolato a un certificato, condiviso pubblicamente tra tutti i mittenti.

In poche parole, il mittente necessita del certificato del destinatario per crittografare un messaggio, mentre il destinatario ha bisogno della chiave privata associata per poterlo decrittografare.

Diamo un'occhiata a come implementare una funzione encryptData () , utilizzando un certificato di crittografia:

public static byte[] encryptData(byte[] data, X509Certificate encryptionCertificate) throws CertificateEncodingException, CMSException, IOException { byte[] encryptedData = null; if (null != data && null != encryptionCertificate) { CMSEnvelopedDataGenerator cmsEnvelopedDataGenerator = new CMSEnvelopedDataGenerator(); JceKeyTransRecipientInfoGenerator jceKey = new JceKeyTransRecipientInfoGenerator(encryptionCertificate); cmsEnvelopedDataGenerator.addRecipientInfoGenerator(transKeyGen); CMSTypedData msg = new CMSProcessableByteArray(data); OutputEncryptor encryptor = new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES128_CBC) .setProvider("BC").build(); CMSEnvelopedData cmsEnvelopedData = cmsEnvelopedDataGenerator .generate(msg,encryptor); encryptedData = cmsEnvelopedData.getEncoded(); } return encryptedData; }

Abbiamo creato un oggetto JceKeyTransRecipientInfoGenerator utilizzando il certificato del destinatario.

Then, we've created a new CMSEnvelopedDataGenerator object and added the recipient information generator into it.

After that, we've used the JceCMSContentEncryptorBuilder class to create an OutputEncrytor object, using the AES CBC algorithm.

The encryptor is used later to generate a CMSEnvelopedData object that encapsulates the encrypted message.

Finally, the encoded representation of the envelope is returned as a byte array.

Now, let's see what the implementation of the decryptData() method looks like:

public static byte[] decryptData( byte[] encryptedData, PrivateKey decryptionKey) throws CMSException { byte[] decryptedData = null; if (null != encryptedData && null != decryptionKey) { CMSEnvelopedData envelopedData = new CMSEnvelopedData(encryptedData); Collection recipients = envelopedData.getRecipientInfos().getRecipients(); KeyTransRecipientInformation recipientInfo = (KeyTransRecipientInformation) recipients.iterator().next(); JceKeyTransRecipient recipient = new JceKeyTransEnvelopedRecipient(decryptionKey); return recipientInfo.getContent(recipient); } return decryptedData; }

First, we've initialized a CMSEnvelopedData object using the encrypted data byte array, and then we've retrieved all the intended recipients of the message using the getRecipients() method.

Once done, we've created a new JceKeyTransRecipient object associated with the recipient's private key.

The recipientInfo instance contains the decrypted/encapsulated message, but we can't retrieve it unless we have the corresponding recipient's key.

Finally, given the recipient key as an argument, the getContent() method returns the raw byte array extracted from the EnvelopedData this recipient is associated with.

Let's write a simple test to make sure everything works exactly as it should:

String secretMessage = "My password is 123456Seven"; System.out.println("Original Message : " + secretMessage); byte[] stringToEncrypt = secretMessage.getBytes(); byte[] encryptedData = encryptData(stringToEncrypt, certificate); System.out.println("Encrypted Message : " + new String(encryptedData)); byte[] rawData = decryptData(encryptedData, privateKey); String decryptedMessage = new String(rawData); System.out.println("Decrypted Message : " + decryptedMessage);

As a result:

Original Message : My password is 123456Seven Encrypted Message : 0�*�H��... Decrypted Message : My password is 123456Seven

4.3 CMS/PKCS7 Signature and Verification

Signature and verification are cryptographic operations that validate the authenticity of data.

Let's see how to sign a secret message using a digital certificate:

public static byte[] signData( byte[] data, X509Certificate signingCertificate, PrivateKey signingKey) throws Exception { byte[] signedMessage = null; List certList = new ArrayList(); CMSTypedData cmsData= new CMSProcessableByteArray(data); certList.add(signingCertificate); Store certs = new JcaCertStore(certList); CMSSignedDataGenerator cmsGenerator = new CMSSignedDataGenerator(); ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA").build(signingKey); cmsGenerator.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( new JcaDigestCalculatorProviderBuilder().setProvider("BC") .build()).build(contentSigner, signingCertificate)); cmsGenerator.addCertificates(certs); CMSSignedData cms = cmsGenerator.generate(cmsData, true); signedMessage = cms.getEncoded(); return signedMessage; } 

First, we've embedded the input into a CMSTypedData, then, we've created a new CMSSignedDataGenerator object.

We've used SHA256withRSA as a signature algorithm, and our signing key to create a new ContentSigner object.

The contentSigner instance is used afterward, along with the signing certificate to create a SigningInfoGenerator object.

After adding the SignerInfoGenerator and the signing certificate to the CMSSignedDataGenerator instance, we finally use the generate() method to create a CMS signed-data object, which also carries a CMS signature.

Now that we've seen how to sign data, let's see how to verify signed data:

public static boolean verifSignedData(byte[] signedData) throws Exception { X509Certificate signCert = null; ByteArrayInputStream inputStream = new ByteArrayInputStream(signedData); ASN1InputStream asnInputStream = new ASN1InputStream(inputStream); CMSSignedData cmsSignedData = new CMSSignedData( ContentInfo.getInstance(asnInputStream.readObject())); SignerInformationStore signers = cmsSignedData.getCertificates().getSignerInfos(); SignerInformation signer = signers.getSigners().iterator().next(); Collection certCollection = certs.getMatches(signer.getSID()); X509CertificateHolder certHolder = certCollection.iterator().next(); return signer .verify(new JcaSimpleSignerInfoVerifierBuilder() .build(certHolder)); }

Again, we've created a CMSSignedData object based on our signed data byte array, then, we've retrieved all signers associated with the signatures using the getSignerInfos() method.

In this example, we've verified only one signer, but for generic use, it is mandatory to iterate over the collection of signers returned by the getSigners() method and check each one separately.

Infine, abbiamo creato un oggetto SignerInformationVerifier utilizzando il metodo build () e lo abbiamo passato al metodo verify () .

Il metodo verify () restituisce true se l'oggetto specificato può verificare con successo la firma sull'oggetto firmatario.

Ecco un semplice esempio:

byte[] signedData = signData(rawData, certificate, privateKey); Boolean check = verifSignData(signedData); System.out.println(check);

Di conseguenza:

true

5. conclusione

In questo articolo, abbiamo scoperto come utilizzare la libreria BouncyCastle per eseguire operazioni crittografiche di base, come crittografia e firma.

In una situazione reale, spesso desideriamo firmare e crittografare i nostri dati, in questo modo solo il destinatario è in grado di decrittografarli utilizzando la chiave privata e verificarne l'autenticità in base alla firma digitale.

Gli snippet di codice possono essere trovati, come sempre, su GitHub.