Pengantar BouncyCastle dengan Java

1. Ikhtisar

BouncyCastle adalah library Java yang melengkapi Java Cryptographic Extension (JCE) default.

Dalam artikel pengantar ini, kami akan menunjukkan cara menggunakan BouncyCastle untuk melakukan operasi kriptografi, seperti enkripsi dan tanda tangan.

2. Konfigurasi Maven

Sebelum kita mulai bekerja dengan pustaka, kita perlu menambahkan dependensi yang diperlukan ke file pom.xml kita :

 org.bouncycastle bcpkix-jdk15on 1.58 

Perhatikan bahwa kami selalu dapat mencari versi dependensi terbaru di Maven Central Repository.

3. Siapkan File Kebijakan Yurisdiksi Kekuatan Tidak Terbatas

Penginstalan Java standar dibatasi dalam hal kekuatan untuk fungsi kriptografi, hal ini disebabkan kebijakan yang melarang penggunaan kunci dengan ukuran yang melebihi nilai tertentu misal 128 untuk AES.

Untuk mengatasi batasan ini, kita perlu mengkonfigurasi file kebijakan yurisdiksi kekuatan tak terbatas .

Untuk melakukan itu, pertama-tama kita perlu mengunduh paket dengan mengikuti tautan ini. Setelah itu, kita perlu mengekstrak file zip ke direktori pilihan kita - yang berisi dua file jar:

  • local_policy.jar
  • US_export_policy.jar

Terakhir, kita perlu mencari folder {JAVA_HOME} / lib / security dan mengganti file kebijakan yang ada dengan yang telah kita ekstrak di sini.

Perhatikan bahwa di Java 9, kita tidak perlu lagi mengunduh paket file kebijakan , menyetel properti crypto.policy ke unlimited sudah cukup:

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

Setelah selesai, kita perlu memeriksa apakah konfigurasi berfungsi dengan benar:

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

Hasil dari:

Max Key Size for AES : 2147483647

Berdasarkan ukuran kunci maksimum yang dikembalikan oleh metode getMaxAllowedKeyLength () , kami dapat dengan aman mengatakan bahwa file kebijakan kekuatan tak terbatas telah diinstal dengan benar.

Jika nilai yang dikembalikan sama dengan 128, kita perlu memastikan bahwa kita telah menginstal file ke JVM tempat kita menjalankan kode.

4. Operasi Kriptografi

4.1. Mempersiapkan Sertifikat dan Kunci Pribadi

Sebelum kita terjun ke implementasi fungsi kriptografi, pertama kita perlu membuat sertifikat dan kunci pribadi.

Untuk tujuan pengujian, kami dapat menggunakan sumber daya ini:

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

Baeldung.cer adalah sertifikat digital yang menggunakan standar infrastruktur kunci publik X.509 internasional, sedangkan Baeldung.p12 adalah Keystore PKCS12 yang dilindungi sandi yang berisi kunci pribadi.

Mari kita lihat bagaimana ini dapat dimuat di 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);

Pertama, kami telah menambahkan BouncyCastleProvider sebagai penyedia keamanan secara dinamis menggunakan metode addProvider () .

Ini juga dapat dilakukan secara statis dengan mengedit file {JAVA_HOME} /jre/lib/security/java.security , dan menambahkan baris ini:

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

Setelah penyedia diinstal dengan benar, kita telah membuat objek CertificateFactory menggunakan metode getInstance () .

Metode getInstance () mengambil dua argumen; jenis sertifikat "X.509", dan penyedia keamanan "BC".

The certFactory contoh selanjutnya digunakan untuk menghasilkan X509Certificate objek, melalui generateCertificate () metode.

Dengan cara yang sama, kita telah membuat objek PKCS12 Keystore, tempat metode load () dipanggil.

Metode getKey () mengembalikan kunci pribadi yang terkait dengan alias yang diberikan.

Perhatikan bahwa PKCS12 Keystore berisi sekumpulan kunci pribadi, setiap kunci pribadi dapat memiliki kata sandi tertentu, itulah mengapa kita memerlukan kata sandi global untuk membuka Keystore, dan kata sandi khusus untuk mengambil kunci pribadi.

Sertifikat dan pasangan kunci privat terutama digunakan dalam operasi kriptografi asimetris:

  • Enkripsi
  • Dekripsi
  • Tanda tangan
  • Verifikasi

4.2 Enkripsi dan Dekripsi CMS / PKCS7

Dalam kriptografi enkripsi asimetris, setiap komunikasi memerlukan sertifikat publik dan kunci privat.

Penerima terikat pada sebuah sertifikat, yang dibagikan secara publik di antara semua pengirim.

Sederhananya, pengirim membutuhkan sertifikat penerima untuk mengenkripsi pesan, sedangkan penerima membutuhkan kunci pribadi terkait untuk dapat mendekripsinya.

Mari kita lihat cara mengimplementasikan fungsi encryptData () , menggunakan sertifikat enkripsi:

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

Kami telah membuat objek JceKeyTransRecipientInfoGenerator menggunakan sertifikat penerima.

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.

Terakhir, kita telah membuat objek SignerInformationVerifier menggunakan metode build () dan meneruskannya ke metode verifikasi () .

Metode verifikasi () mengembalikan nilai true jika objek yang diberikan berhasil memverifikasi tanda tangan pada objek penanda tangan.

Berikut contoh sederhananya:

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

Hasil dari:

true

5. Kesimpulan

Di artikel ini, kami telah menemukan cara menggunakan pustaka BouncyCastle untuk melakukan operasi kriptografi dasar, seperti enkripsi dan tanda tangan.

Dalam situasi dunia nyata, kami sering ingin menandatangani lalu mengenkripsi data kami, dengan begitu, hanya penerima yang dapat mendekripsinya menggunakan kunci pribadi, dan memeriksa keasliannya berdasarkan tanda tangan digital.

Cuplikan kode dapat ditemukan, seperti biasa, di GitHub.