Einführung in BouncyCastle with Java

1. Überblick

BouncyCastle ist eine Java-Bibliothek, die die standardmäßige Java Cryptographic Extension (JCE) ergänzt.

In diesem einleitenden Artikel wird gezeigt, wie Sie mit BouncyCastle kryptografische Operationen wie Verschlüsselung und Signatur ausführen.

2. Maven Konfiguration

Bevor wir mit der Bibliothek arbeiten können, müssen wir die erforderlichen Abhängigkeiten in unsere Datei pom.xml einfügen:

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

Beachten Sie, dass wir die neuesten Versionen der Abhängigkeiten immer im https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org.bouncy%22%20 [Maven Central Repository nachschlagen können.

3. Richten Sie Richtliniendateien mit unlimitierter Stärke ein

Die Standard-Java-Installation ist hinsichtlich der Stärke für kryptografische Funktionen begrenzt. Dies ist auf Richtlinien zurückzuführen, die die Verwendung eines Schlüssels mit einer Größe verbieten, die bestimmte Werte überschreitet, z. 128 für AES.

Um diese Einschränkung zu überwinden, müssen wir die Richtliniendateien mit uneingeschränkter Stärke konfigurieren.

Dazu müssen wir das Paket zunächst herunterladen, indem Sie diesem Link folgen.

Anschließend müssen wir die gezippte Datei in ein Verzeichnis unserer Wahl extrahieren, das zwei JAR-Dateien enthält:

  • local policy.jar__

  • US export policy.jar

Schließlich müssen wir nach dem Ordner \ {JAVA HOME}/lib/security__ suchen und die vorhandenen Richtliniendateien durch die hier extrahierten ersetzen.

Beachten Sie, dass in Java 9 das Paket mit den Richtliniendateien nicht mehr heruntergeladen werden muss.

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

Anschließend müssen wir überprüfen, ob die Konfiguration korrekt funktioniert:

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

Als Ergebnis:

Max Key Size for AES : 2147483647

Basierend auf der maximalen Schlüsselgröße, die von der Methode getMaxAllowedKeyLength () zurückgegeben wird, können wir mit Sicherheit sagen, dass die Richtliniendateien mit unbegrenzter Stärke korrekt installiert wurden.

Wenn der zurückgegebene Wert 128 entspricht, müssen wir sicherstellen, dass wir die Dateien in der JVM installiert haben, in der der Code ausgeführt wird.

4. Kryptographische Operationen

4.1. Zertifikat und privaten Schlüssel vorbereiten

Bevor wir in die Implementierung kryptografischer Funktionen einsteigen, müssen wir zuerst ein Zertifikat und einen privaten Schlüssel erstellen.

Zu Testzwecken können wir diese Ressourcen verwenden:

(Passwort = "Passwort")]

Baeldung.cer ist ein digitales Zertifikat, das den internationalen X.509-Infrastrukturstandard für öffentliche Schlüssel verwendet, während Baeldung.p12 ein kennwortgeschützter PKCS12 Keystore ist, der einen privaten Schlüssel enthält Schlüssel.

Mal sehen, wie diese in Java geladen werden können:

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

Zuerst haben wir den BouncyCastleProvider als Sicherheitsanbieter dynamisch hinzugefügt, indem die addProvider () -Methode verwendet wird.

Dies kann auch statisch erfolgen, indem Sie die Datei \ {JAVA HOME}/jre/lib/security/java.security__ bearbeiten und folgende Zeile hinzufügen:

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

Nachdem der Provider ordnungsgemäß installiert wurde, haben wir ein CertificateFactory -Objekt mit der getInstance () -Methode erstellt

Die getInstance () - Methode benötigt zwei Argumente. der Zertifikattyp „X.509“ und der Sicherheitsanbieter „BC“.

Die certFactory -Instanz wird anschließend zum Generieren eines X509Certificate -Objekts über die generateCertificate () -Methode verwendet.

Auf dieselbe Weise haben wir ein PKCS12-Keystore-Objekt erstellt, in dem die Methode load () aufgerufen wird.

Die Methode getKey () gibt den privaten Schlüssel zurück, der einem bestimmten Alias ​​zugeordnet ist.

Beachten Sie, dass ein PKCS12-Keystore einen Satz privater Schlüssel enthält. Jeder private Schlüssel kann ein bestimmtes Kennwort haben. Aus diesem Grund benötigen wir ein globales Kennwort, um den Keystore zu öffnen, und ein bestimmtes, um den privaten Schlüssel abzurufen.

Das Zertifikat und das private Schlüsselpaar werden hauptsächlich bei asymmetrischen kryptographischen Operationen verwendet:

  • Verschlüsselung

  • Entschlüsselung

  • Unterschrift

  • Überprüfung

4.2 CMS/PKCS7-Verschlüsselung und Entschlüsselung

Bei der asymmetrischen Verschlüsselungskryptografie erfordert jede Kommunikation ein öffentliches Zertifikat und einen privaten Schlüssel. **

Der Empfänger ist an ein Zertifikat gebunden, das öffentlich von allen Absendern gemeinsam genutzt wird.

Vereinfacht gesagt, benötigt der Sender das Zertifikat des Empfängers, um eine Nachricht zu verschlüsseln, während der Empfänger den zugehörigen privaten Schlüssel benötigt, um die Nachricht entschlüsseln zu können.

Sehen wir uns an, wie eine encryptData () - Funktion mit einem Verschlüsselungszertifikat implementiert wird:

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

Wir haben ein JceKeyTransRecipientInfoGenerator -Objekt mit dem Zertifikat des Empfängers erstellt.

Dann haben wir ein neues CMSEnvelopedDataGenerator -Objekt erstellt und den Empfängerinformationsgenerator hinzugefügt.

Danach haben wir die Klasse JceCMSContentEncryptorBuilder verwendet, um ein OutputEncrytor -Objekt unter Verwendung des AES CBC-Algorithmus zu erstellen.

Der Verschlüsseler wird später zum Generieren eines CMSEnvelopedData -Objekts verwendet, das die verschlüsselte Nachricht enthält.

Schließlich wird die codierte Darstellung des Umschlags als Byte-Array zurückgegeben.

Lassen Sie uns nun sehen, wie die Implementierung der decryptData () -Methode aussieht:

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

Zunächst haben wir ein CMSEnvelopedData -Objekt mit dem verschlüsselten Datenbyte-Array initialisiert und anschließend alle beabsichtigten Empfänger der Nachricht mit der getRecipients () -Methode abgerufen.

Anschließend haben wir ein neues JceKeyTransRecipient -Objekt erstellt, das dem privaten Schlüssel des Empfängers zugeordnet ist.

Die recipientInfo -Instanz enthält die entschlüsselte/gekapselte Nachricht, die jedoch nicht abgerufen werden kann, wenn der Schlüssel des entsprechenden Empfängers nicht vorhanden ist.

  • Wenn der Empfängerschlüssel als Argument angegeben wird, gibt die getContent () -Methode schließlich das aus dem EnvelopedData ** extrahierte unformatierte Byte-Array zurück.

Schreiben wir einen einfachen Test, um sicherzustellen, dass alles genau so funktioniert, wie es sollte:

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

Als Ergebnis:

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

4.2 CMS/PKCS7-Signatur und Überprüfung

Signatur und Verifizierung sind kryptografische Operationen, die die Authentizität von Daten überprüfen.

Mal sehen, wie man eine geheime Nachricht mit einem digitalen Zertifikat signiert:

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

Zuerst haben wir die Eingabe in ein CMSTypedData eingebettet, und dann haben wir ein neues CMSSignedDataGenerator -Objekt erstellt.

Wir haben SHA256 withRSA als Signaturalgorithmus und unseren Signaturschlüssel verwendet, um ein neues ContentSigner -Objekt zu erstellen.

Die contentSigner -Instanz wird anschließend zusammen mit dem Signaturzertifikat zum Erstellen eines SigningInfoGenerator -Objekts verwendet.

Nach dem Hinzufügen des SignerInfoGenerator und des Signaturzertifikats zur CMSSignedDataGenerator -Instanz verwenden wir schließlich die Methode generate () , um ein CMS-Objekt mit signierten Daten zu erstellen, das auch eine CMS-Signatur enthält.

Nun, da wir gesehen haben, wie man Daten signiert, sehen wir, wie signierte Daten überprüft werden

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

Wir haben wieder ein CMSSignedData -Objekt basierend auf unserem signierten Datenbyte-Array erstellt. Dann haben wir alle mit den Signaturen verknüpften Unterzeichner mit der getSignerInfos () -Methode abgerufen.

In diesem Beispiel haben wir nur einen Unterzeichner überprüft. Für die generische Verwendung ist es jedoch zwingend erforderlich, die von der Methode getSigners () zurückgegebene Auflistung von Unterzeichnern zu durchlaufen und einzeln zu prüfen.

Schließlich haben wir ein SignerInformationVerifier -Objekt mit der build () -Methode erstellt und an die verify () -Methode übergeben.

Die Methode verify () gibt true zurück, wenn das angegebene Objekt die Signatur des Unterzeichnerobjekts erfolgreich überprüfen kann.

Hier ist ein einfaches Beispiel:

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

Als Ergebnis:

true

5. Fazit

In diesem Artikel haben wir entdeckt, wie Sie die BouncyCastle-Bibliothek verwenden, um grundlegende kryptografische Operationen wie Verschlüsselung und Signatur auszuführen.

In einer realen Situation möchten wir häufig unsere Daten signieren, dann verschlüsseln. Auf diese Weise kann nur der Empfänger sie mit dem privaten Schlüssel entschlüsseln und anhand der digitalen Signatur seine Authentizität überprüfen.

Die Code-Snippets finden Sie wie immer unter https://github.com/eugenp/tutorials/tree/master/libraries.html bei GitHub].