Introduction à BouncyCastle avec Java

1. Vue d’ensemble

BouncyCastle est une bibliothèque Java qui complète l’extension Java Cryptographic Extension (JCE) par défaut.

Dans cet article d’introduction, nous allons montrer comment utiliser BouncyCastle pour effectuer des opérations cryptographiques, telles que le cryptage et la signature.

2. Configuration Maven

Avant de commencer à utiliser la bibliothèque, nous devons ajouter les dépendances requises à notre fichier pom.xml :

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
    <version>1.58</version>
</dependency>

Notez que nous pouvons toujours rechercher les dernières versions de dépendances dans le Maven Central Repository .

3. Configuration de fichiers de stratégie juridictionnelle de compétence illimitée

L’installation Java standard est limitée en termes de puissance pour les fonctions cryptographiques, en raison de règles interdisant l’utilisation d’une clé dont la taille dépasse certaines valeurs, par exemple. 128 pour AES.

Pour surmonter cette limitation, nous devons configurer les fichiers de stratégie de compétence illimitée.

Pour ce faire, nous devons d’abord télécharger le package en suivant le lien http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html ].

Ensuite, nous devons extraire le fichier compressé dans un répertoire de notre choix, qui contient deux fichiers jar:

  • local policy.jar__

  • US export policy.jar

Enfin, nous devons rechercher le dossier \ {JAVA HOME}/lib/security__ et remplacer les fichiers de règles existants par ceux que nous avons extraits ici.

Notez que sous Java 9, nous n’avons plus besoin de télécharger le paquet de fichiers de règles , il suffit de définir la propriété crypto.policy sur unlimited

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

Une fois cela fait, nous devons vérifier que la configuration fonctionne correctement:

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

Par conséquent:

Max Key Size for AES : 2147483647

En fonction de la taille de clé maximale renvoyée par la méthode getMaxAllowedKeyLength () , nous pouvons dire en toute sécurité que les fichiers de stratégie de force illimitée ont été installés correctement.

Si la valeur renvoyée est égale à 128, nous devons nous assurer que nous avons installé les fichiers dans la machine virtuelle Java sur laquelle nous exécutons le code.

4. Opérations cryptographiques

4.1. Préparation du certificat et de la clé privée

Avant de nous lancer dans la mise en œuvre de fonctions cryptographiques, nous devons d’abord créer un certificat et une clé privée.

À des fins de test, nous pouvons utiliser ces ressources:

(mot de passe = “mot de passe”)]

Baeldung.cer est un certificat numérique qui utilise le standard international d’infrastructure de clé publique X.509, tandis que le Baeldung.p12 est un fichier de clés protégé par un mot de passe clé.

Voyons comment ils peuvent être chargés en 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);

Tout d’abord, nous avons ajouté BouncyCastleProvider en tant que fournisseur de sécurité de manière dynamique à l’aide de la méthode addProvider () .

Cela peut également être fait de manière statique en modifiant le fichier \ {JAVA HOME}/jre/lib/security/java.security__ et en ajoutant cette ligne:

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

Une fois le fournisseur correctement installé, nous avons créé un objet CertificateFactory à l’aide de la méthode getInstance () .

La méthode getInstance () prend deux arguments; le type de certificat “X.509” et le fournisseur de sécurité “BC”.

L’instance certFactory est ensuite utilisée pour générer un objet X509Certificate , via la méthode generateCertificate () .

De la même manière, nous avons créé un objet PKCS12 Keystore, sur lequel la méthode load () est appelée.

La méthode getKey () renvoie la clé privée associée à un alias donné.

Notez qu’un magasin de clés PKCS12 contient un ensemble de clés privées. Chaque clé privée peut avoir un mot de passe spécifique. C’est pourquoi nous avons besoin d’un mot de passe global pour ouvrir le magasin de clés et d’un mot de passe spécifique pour récupérer la clé privée.

Le certificat et la paire de clés privées sont principalement utilisés dans les opérations cryptographiques asymétriques:

  • Cryptage

  • Décryptage

  • Signature

  • Vérification

4.2 Chiffrement et déchiffrement CMS/PKCS7

  • Dans la cryptographie à cryptage asymétrique, chaque communication nécessite un certificat public et une clé privée. **

Le destinataire est lié à un certificat, qui est partagé publiquement entre tous les expéditeurs.

En bref, l’expéditeur a besoin du certificat du destinataire pour chiffrer un message, tandis que le destinataire a besoin de la clé privée associée pour le déchiffrer.

Voyons comment implémenter une fonction encryptData () à l’aide d’un certificat de chiffrement:

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;
}

Nous avons créé un objet JceKeyTransRecipientInfoGenerator à l’aide du certificat du destinataire.

Ensuite, nous avons créé un nouvel objet CMSEnvelopedDataGenerator et y avons ajouté le générateur d’informations sur le destinataire.

Nous avons ensuite utilisé la classe JceCMSContentEncryptorBuilder pour créer un objet OutputEncrytor à l’aide de l’algorithme AES CBC.

Le crypteur est utilisé ultérieurement pour générer un objet CMSEnvelopedData qui encapsule le message crypté.

Enfin, la représentation codée de l’enveloppe est renvoyée sous forme de tableau d’octets.

Voyons maintenant à quoi ressemble l’implémentation de la méthode decryptData () :

public static byte[]decryptData(
  byte[]encryptedData,
  PrivateKey decryptionKey)
  throws CMSException {

    byte[]decryptedData = null;
    if (null != encryptedData && null != decryptionKey) {
        CMSEnvelopedData envelopedData = new CMSEnvelopedData(encryptedData);

        Collection<RecipientInformation> recipients
          = envelopedData.getRecipientInfos().getRecipients();
        KeyTransRecipientInformation recipientInfo
          = (KeyTransRecipientInformation) recipients.iterator().next();
        JceKeyTransRecipient recipient
          = new JceKeyTransEnvelopedRecipient(decryptionKey);

        return recipientInfo.getContent(recipient);
    }
    return decryptedData;
}

Tout d’abord, nous avons initialisé un objet CMSEnvelopedData à l’aide du tableau d’octets de données cryptés, puis nous avons extrait tous les destinataires du message à l’aide de la méthode getRecipients () .

Une fois cela fait, nous avons créé un nouvel objet JceKeyTransTransecipient associé à la clé privée du destinataire.

L’instance recipientInfo contient le message déchiffré/encapsulé, mais nous ne pouvons le récupérer que si nous avons la clé du destinataire correspondante.

  • Enfin, étant donné que la clé du destinataire est un argument, la méthode getContent () renvoie le tableau d’octets brut extrait du EnvelopedData ** auquel ce destinataire est associé.

Faisons un test simple pour nous assurer que tout fonctionne exactement comme il se doit:

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);

Par conséquent:

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

4.2 Signature CMS/PKCS7 et Vérification

La signature et la vérification sont des opérations cryptographiques qui valident l’authenticité des données.

Voyons comment signer un message secret à l’aide d’un certificat numérique:

public static byte[]signData(
  byte[]data,
  X509Certificate signingCertificate,
  PrivateKey signingKey) throws Exception {

    byte[]signedMessage = null;
    List<X509Certificate> certList = new ArrayList<X509Certificate>();
    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;
}

Nous avons d’abord intégré l’entrée dans un objet CMSTypedData , puis nous avons créé un nouvel objet CMSSignedDataGenerator .

Nous avons utilisé SHA256withRSA comme algorithme de signature et notre clé de signature pour créer un nouvel objet ContentSigner .

L’instance contentSigner est utilisée par la suite, avec le certificat de signature pour créer un objet SigningInfoGenerator .

Après avoir ajouté SignerInfoGenerator et le certificat de signature à l’instance CMSSignedDataGenerator , nous avons finalement utilisé la méthode generate () pour créer un objet CMS signé-data, qui comporte également une signature CMS.

Maintenant que nous avons vu comment signer les données, voyons comment vérifier les données signées:

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<X509CertificateHolder> certCollection
      = certs.getMatches(signer.getSID());
    X509CertificateHolder certHolder = certCollection.iterator().next();

    return signer
      .verify(new JcaSimpleSignerInfoVerifierBuilder()
      .build(certHolder));
}

De nouveau, nous avons créé un objet CMSSignedData basé sur notre tableau d’octets de données signés, puis nous avons récupéré tous les signataires associés aux signatures à l’aide de la méthode getSignerInfos () .

Dans cet exemple, nous n’avons vérifié qu’un seul signataire, mais pour une utilisation générique, il est obligatoire de parcourir la collection de signataires renvoyés par la méthode getSigners () et de les vérifier séparément.

Enfin, nous avons créé un objet SignerInformationVerifier à l’aide de la méthode build () et nous l’avons transmis à la méthode verify () .

La méthode verify () retourne true si l’objet donné peut vérifier la signature sur l’objet signataire

Voici un exemple simple:

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

Par conséquent:

true

5. Conclusion

Dans cet article, nous avons découvert comment utiliser la bibliothèque BouncyCastle pour effectuer des opérations cryptographiques de base, telles que le cryptage et la signature.

Dans une situation réelle, nous voulons souvent signer puis chiffrer nos données. Ainsi, seul le destinataire est en mesure de les déchiffrer à l’aide de la clé privée et de vérifier son authenticité sur la base de la signature numérique.

Les extraits de code peuvent être trouvés, comme toujours, over sur GitHub .