De Advanced Encryption Standard (AES, Rijndael) is een blokcijfer encryptie en decryptie algoritme, het meest gebruikte encryptie algoritme in de wereld. De AES verwerkt blokken van 128 bits met een geheime sleutel van 128, 192 of 256 bits.
Dit artikel laat een paar voorbeelden zien van Java AES encryptie en decryptie:
- AES String encryptie – (versleutelen en ontsleutelen van een string).
- AES Wachtwoord-gebaseerde encryptie – (De geheime sleutel wordt afgeleid van een gegeven wachtwoord).
- AES Bestand encryptie. (op basis van een wachtwoord).
In dit artikel richten we ons op de 256-bits AES-encryptie met Galois Counter Mode (GCM).
GCM = CTR + Authentication.
Verder lezen
Lees dit – NIST – Aanbeveling voor Galois/Counter Mode (GCM)
Gebruik geen AES Electronic codebook (ECB)-modus
De AES ECB-modus, of AES/ECB/PKCS5Padding
(in Java) is semantisch niet veilig – De door de ECB gecodeerde cijfertekst kan informatie over de onbewerkte tekst lekken. Hier is een discussie over Waarom zou ik geen ECB-encryptie gebruiken?
Java en AES-encryptie-ingangen.
In AES-encryptie en -decryptie hebben we de volgende ingangen nodig:
AES-encryptie best practice
Gebruik IV niet opnieuw met dezelfde sleutel.
1.1 De IV (initial value of initiële vector), dit zijn willekeurige bytes, meestal 12 bytes of 16 bytes. In Java kunnen we SecureRandom
gebruiken om de willekeurige IV te genereren.
// 16 bytes IV public static byte getRandomNonce() { byte nonce = new byte; new SecureRandom().nextBytes(nonce); return nonce; } // 12 bytes IV public static byte getRandomNonce() { byte nonce = new byte; new SecureRandom().nextBytes(nonce); return nonce; }
1.2 De AES geheime sleutel, ofwel AES-128
of AES-256
. In Java kunnen we KeyGenerator
gebruiken om de AES geheime sleutel te genereren.
// 256 bits AES secret key public static SecretKey getAESKey() throws NoSuchAlgorithmException { KeyGenerator keyGen = KeyGenerator.getInstance("AES"); keyGen.init(256, SecureRandom.getInstanceStrong()); return keyGen.generateKey(); }
1.3 De AES geheime sleutel die is afgeleid van een gegeven wachtwoord. In Java kunnen we de SecretKeyFactory
en PBKDF2WithHmacSHA256
gebruiken om een AES-sleutel te genereren uit een gegeven wachtwoord.
// AES key derived from a password public static SecretKey getAESKeyFromPassword(char password, byte salt) throws NoSuchAlgorithmException, InvalidKeySpecException { SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); // iterationCount = 65536 // keyLength = 256 KeySpec spec = new PBEKeySpec(password, salt, 65536, 256); SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); return secret; }
We gebruiken salt
om regenboog-aanvallen te beschermen, en het is ook een willekeurige byte, we kunnen dezelfde 1.1 getRandomNonce
gebruiken om hem te genereren.
1.4 We groeperen de bovenstaande methoden in een enkele util
klasse, zodat we niet steeds dezelfde code herhalen.
package com.mkyong.crypto.utils;import javax.crypto.KeyGenerator;import javax.crypto.SecretKey;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.PBEKeySpec;import javax.crypto.spec.SecretKeySpec;import java.security.NoSuchAlgorithmException;import java.security.SecureRandom;import java.security.spec.InvalidKeySpecException;import java.security.spec.KeySpec;import java.util.ArrayList;import java.util.List;public class CryptoUtils { public static byte getRandomNonce(int numBytes) { byte nonce = new byte; new SecureRandom().nextBytes(nonce); return nonce; } // AES secret key public static SecretKey getAESKey(int keysize) throws NoSuchAlgorithmException { KeyGenerator keyGen = KeyGenerator.getInstance("AES"); keyGen.init(keysize, SecureRandom.getInstanceStrong()); return keyGen.generateKey(); } // Password derived AES 256 bits secret key public static SecretKey getAESKeyFromPassword(char password, byte salt) throws NoSuchAlgorithmException, InvalidKeySpecException { SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); // iterationCount = 65536 // keyLength = 256 KeySpec spec = new PBEKeySpec(password, salt, 65536, 256); SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); return secret; } // hex representation public static String hex(byte bytes) { StringBuilder result = new StringBuilder(); for (byte b : bytes) { result.append(String.format("%02x", b)); } return result.toString(); } // print hex with block size split public static String hexWithBlockSize(byte bytes, int blockSize) { String hex = hex(bytes); // one hex = 2 chars blockSize = blockSize * 2; // better idea how to print this? List<String> result = new ArrayList<>(); int index = 0; while (index < hex.length()) { result.add(hex.substring(index, Math.min(index + blockSize, hex.length()))); index += blockSize; } return result.toString(); }}
AES encryptie en decryptie.
De AES-GSM is de meest gebruikte geauthenticeerde cipher. In dit voorbeeld wordt een string versleuteld en ontsleuteld met 256-bits AES in Galois Counter Mode (GCM).
De AES-GCM-ingangen:
- AES Secret key (256 bits)
- IV – 96 bits (12 bytes)
- Lengte (in bits) van authenticatie-tag – 128 bits (16 bytes)
2.1 In Java gebruiken we AES/GCM/NoPadding
om het AES-GCM
algoritme weer te geven. Voor de versleutelde uitvoer voegen we de 16 bytes IV toe aan de versleutelde tekst (cijfertekst), omdat we dezelfde IV nodig hebben voor het ontcijferen.
Is dit ok als IV publiekelijk bekend is?
Het is OK als IV publiekelijk bekend is, het enige geheim is de sleutel, houd deze privé en vertrouwelijk.
Dit voorbeeld gebruikt AES om een onbewerkte tekst te versleutelen Hello World AES-GCM
en deze later weer te ontsleutelen naar de oorspronkelijke onbewerkte tekst.
package com.mkyong.crypto.encryptor;import com.mkyong.crypto.utils.CryptoUtils;import javax.crypto.Cipher;import javax.crypto.SecretKey;import javax.crypto.spec.GCMParameterSpec;import java.nio.ByteBuffer;import java.nio.charset.Charset;import java.nio.charset.StandardCharsets;/** * AES-GCM inputs - 12 bytes IV, need the same IV and secret keys for encryption and decryption. * <p> * The output consist of iv, encrypted content, and auth tag in the following format: * output = byte {i i i c c c c c c ...} * <p> * i = IV bytes * c = content bytes (encrypted content, auth tag) */public class EncryptorAesGcm { private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding"; private static final int TAG_LENGTH_BIT = 128; private static final int IV_LENGTH_BYTE = 12; private static final int AES_KEY_BIT = 256; private static final Charset UTF_8 = StandardCharsets.UTF_8; // AES-GCM needs GCMParameterSpec public static byte encrypt(byte pText, SecretKey secret, byte iv) throws Exception { Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); cipher.init(Cipher.ENCRYPT_MODE, secret, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); byte encryptedText = cipher.doFinal(pText); return encryptedText; } // prefix IV length + IV bytes to cipher text public static byte encryptWithPrefixIV(byte pText, SecretKey secret, byte iv) throws Exception { byte cipherText = encrypt(pText, secret, iv); byte cipherTextWithIv = ByteBuffer.allocate(iv.length + cipherText.length) .put(iv) .put(cipherText) .array(); return cipherTextWithIv; } public static String decrypt(byte cText, SecretKey secret, byte iv) throws Exception { Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); cipher.init(Cipher.DECRYPT_MODE, secret, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); byte plainText = cipher.doFinal(cText); return new String(plainText, UTF_8); } public static String decryptWithPrefixIV(byte cText, SecretKey secret) throws Exception { ByteBuffer bb = ByteBuffer.wrap(cText); byte iv = new byte; bb.get(iv); //bb.get(iv, 0, iv.length); byte cipherText = new byte; bb.get(cipherText); String plainText = decrypt(cipherText, secret, iv); return plainText; } public static void main(String args) throws Exception { String OUTPUT_FORMAT = "%-30s:%s"; String pText = "Hello World AES-GCM, Welcome to Cryptography!"; // encrypt and decrypt need the same key. // get AES 256 bits (32 bytes) key SecretKey secretKey = CryptoUtils.getAESKey(AES_KEY_BIT); // encrypt and decrypt need the same IV. // AES-GCM needs IV 96-bit (12 bytes) byte iv = CryptoUtils.getRandomNonce(IV_LENGTH_BYTE); byte encryptedText = EncryptorAesGcm.encryptWithPrefixIV(pText.getBytes(UTF_8), secretKey, iv); System.out.println("\n------ AES GCM Encryption ------"); System.out.println(String.format(OUTPUT_FORMAT, "Input (plain text)", pText)); System.out.println(String.format(OUTPUT_FORMAT, "Key (hex)", CryptoUtils.hex(secretKey.getEncoded()))); System.out.println(String.format(OUTPUT_FORMAT, "IV (hex)", CryptoUtils.hex(iv))); System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (hex) ", CryptoUtils.hex(encryptedText))); System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (hex) (block = 16)", CryptoUtils.hexWithBlockSize(encryptedText, 16))); System.out.println("\n------ AES GCM Decryption ------"); System.out.println(String.format(OUTPUT_FORMAT, "Input (hex)", CryptoUtils.hex(encryptedText))); System.out.println(String.format(OUTPUT_FORMAT, "Input (hex) (block = 16)", CryptoUtils.hexWithBlockSize(encryptedText, 16))); System.out.println(String.format(OUTPUT_FORMAT, "Key (hex)", CryptoUtils.hex(secretKey.getEncoded()))); String decryptedText = EncryptorAesGcm.decryptWithPrefixIV(encryptedText, secretKey); System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText)); }}
Uitvoer
Plain Text : Hello World AES-GCM
------ AES GCM Encryption ------Input (plain text) :Hello World AES-GCMKey (hex) :603d87185bf855532f14a77a91ec7b025c004bf664e9f5c6e95613ee9577f436IV (hex) :bdb271ce5235996a0709e09cEncrypted (hex) :bdb271ce5235996a0709e09c2d03eefe319e9329768724755c56291aecaef88cd1e6bdf72b8c7b54d75a94e66b0cd3Encrypted (hex) (block = 16) :------ AES GCM Decryption ------Input (hex) :bdb271ce5235996a0709e09c2d03eefe319e9329768724755c56291aecaef88cd1e6bdf72b8c7b54d75a94e66b0cd3Input (hex) (block = 16) :Key (hex) :603d87185bf855532f14a77a91ec7b025c004bf664e9f5c6e95613ee9577f436Decrypted (plain text) :Hello World AES-GCM
Plain Text : Hello World AES-GCM, Welcome to Cryptography!
------ AES GCM Encryption ------Input (plain text) :Hello World AES-GCM, Welcome to Cryptography!Key (hex) :ddc24663d104e1c2f81f11aef98156503dafdc435f81e3ac3d705015ebab095cIV (hex) :b05d6aedf023f73b9e1e2d11Encrypted (hex) :b05d6aedf023f73b9e1e2d11f6f5137d971aea8c5cdd5b045e0960eb4408e0ee4635cccc2dfeec2c13a89bd400f659be82dc2329e9c36e3b032f38bd42296a8495ac840b0625c097d9Encrypted (hex) (block = 16) :------ AES GCM Decryption ------Input (hex) :b05d6aedf023f73b9e1e2d11f6f5137d971aea8c5cdd5b045e0960eb4408e0ee4635cccc2dfeec2c13a89bd400f659be82dc2329e9c36e3b032f38bd42296a8495ac840b0625c097d9Input (hex) (block = 16) :Key (hex) :ddc24663d104e1c2f81f11aef98156503dafdc435f81e3ac3d705015ebab095cDecrypted (plain text) :Hello World AES-GCM, Welcome to Cryptography!
AES versleuteling en ontsleuteling op basis van een wachtwoord.
Voor wachtwoordgebaseerde encryptie kunnen we gebruik maken van de Password-Based Cryptography Specification (PKCS), gedefinieerd RFC 8018, om een sleutel te genereren uit een gegeven wachtwoord.
Voor PKCS invoer:
- Wachtwoord, u verstrekt dit.
- Zout – Ten minste 64 bits (8 bytes) willekeurig.
- Iteratietelling – Beveel een minimum iteratietelling van 1.000.
Wat is zout en iteratietelling?
- De
salt
produceert een brede set sleutels voor een gegeven wachtwoord. Bijvoorbeeld, als het zout 128 bits is, zullen er maar liefst 2^128 sleutels zijn voor elk wachtwoord. Daarom verhoogt het de moeilijkheidsgraad van rainbow attacks. Bovendien wordt de rainbow table die aanvallers bouwen voor het wachtwoord van een gebruiker nutteloos voor een andere gebruiker. - De
iteration count
verhoogt de kosten van het produceren van sleutels van een wachtwoord, dus het verhogen van de moeilijkheidsgraad en vertragen de snelheid van de aanvallen.
3.1 Voor de versleutelde uitvoer voegen we de 12 bytes IV
en password salt
aan de cijfertekst toe, omdat we voor de ontcijfering dezelfde IV en wachtwoordzout (voor de geheime sleutel) nodig hebben. Verder gebruiken we Base64
encoder om de versleutelde tekst in een string-representatie te coderen, zodat we de versleutelde tekst of cijfertekst in string-formaat kunnen verzenden (was byte array).
Is dit ok als wachtwoordzout publiekelijk bekend is?
Het is hetzelfde met IV, en het is ok als wachtwoordzout publiekelijk bekend is, het enige geheim is de sleutel, waardoor deze privé en vertrouwelijk blijft.
package com.mkyong.crypto.encryptor;import com.mkyong.crypto.utils.CryptoUtils;import javax.crypto.Cipher;import javax.crypto.SecretKey;import javax.crypto.spec.GCMParameterSpec;import java.nio.ByteBuffer;import java.nio.charset.Charset;import java.nio.charset.StandardCharsets;import java.util.Base64;/** * AES-GCM inputs - 12 bytes IV, need the same IV and secret keys for encryption and decryption. * <p> * The output consist of iv, password's salt, encrypted content and auth tag in the following format: * output = byte {i i i s s s c c c c c c ...} * <p> * i = IV bytes * s = Salt bytes * c = content bytes (encrypted content) */public class EncryptorAesGcmPassword { private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding"; private static final int TAG_LENGTH_BIT = 128; // must be one of {128, 120, 112, 104, 96} private static final int IV_LENGTH_BYTE = 12; private static final int SALT_LENGTH_BYTE = 16; private static final Charset UTF_8 = StandardCharsets.UTF_8; // return a base64 encoded AES encrypted text public static String encrypt(byte pText, String password) throws Exception { // 16 bytes salt byte salt = CryptoUtils.getRandomNonce(SALT_LENGTH_BYTE); // GCM recommended 12 bytes iv? byte iv = CryptoUtils.getRandomNonce(IV_LENGTH_BYTE); // secret key from password SecretKey aesKeyFromPassword = CryptoUtils.getAESKeyFromPassword(password.toCharArray(), salt); Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); // ASE-GCM needs GCMParameterSpec cipher.init(Cipher.ENCRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); byte cipherText = cipher.doFinal(pText); // prefix IV and Salt to cipher text byte cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + salt.length + cipherText.length) .put(iv) .put(salt) .put(cipherText) .array(); // string representation, base64, send this string to other for decryption. return Base64.getEncoder().encodeToString(cipherTextWithIvSalt); } // we need the same password, salt and iv to decrypt it private static String decrypt(String cText, String password) throws Exception { byte decode = Base64.getDecoder().decode(cText.getBytes(UTF_8)); // get back the iv and salt from the cipher text ByteBuffer bb = ByteBuffer.wrap(decode); byte iv = new byte; bb.get(iv); byte salt = new byte; bb.get(salt); byte cipherText = new byte; bb.get(cipherText); // get back the aes key from the same password and salt SecretKey aesKeyFromPassword = CryptoUtils.getAESKeyFromPassword(password.toCharArray(), salt); Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); cipher.init(Cipher.DECRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); byte plainText = cipher.doFinal(cipherText); return new String(plainText, UTF_8); } public static void main(String args) throws Exception { String OUTPUT_FORMAT = "%-30s:%s"; String PASSWORD = "this is a password"; String pText = "AES-GSM Password-Bases encryption!"; String encryptedTextBase64 = EncryptorAesGcmPassword.encrypt(pText.getBytes(UTF_8), PASSWORD); System.out.println("\n------ AES GCM Password-based Encryption ------"); System.out.println(String.format(OUTPUT_FORMAT, "Input (plain text)", pText)); System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (base64) ", encryptedTextBase64)); System.out.println("\n------ AES GCM Password-based Decryption ------"); System.out.println(String.format(OUTPUT_FORMAT, "Input (base64)", encryptedTextBase64)); String decryptedText = EncryptorAesGcmPassword.decrypt(encryptedTextBase64, PASSWORD); System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText)); }}
Output
------ AES GCM Password-based Encryption ------Input (plain text) :AES-GSM Password-Bases encryption!Encrypted (base64) :KmrvjnMusJTQo/hB7T5BvlQpvi3bVbdjpZP51NT7I/enrIfSQuDfSK6iXgdPzvUP2IE54mwrKiyHqMkG8224lRZ9tXHcclmdh98I8b3B------ AES GCM Password-based Decryption ------Input (base64) :KmrvjnMusJTQo/hB7T5BvlQpvi3bVbdjpZP51NT7I/enrIfSQuDfSK6iXgdPzvUP2IE54mwrKiyHqMkG8224lRZ9tXHcclmdh98I8b3BDecrypted (plain text) :AES-GSM Password-Bases encryption!
3.2 Als wachtwoord niet overeenkomt, gooit Java AEADBadTagException: Tag mismatch!
// change the password to something else String decryptedText = EncryptorAesGcmPassword.decrypt(encryptedTextBase64, "other password"); System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText));
Output
Exception in thread "main" javax.crypto.AEADBadTagException: Tag mismatch! at java.base/com.sun.crypto.provider.GaloisCounterMode.decryptFinal(GaloisCounterMode.java:623) at java.base/com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1118) at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1055) at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:855) at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446) at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2207) at com.mkyong.crypto.encryptor.EncryptorAesGcmPassword.decrypt(EncryptorAesGcmPassword.java:88) at com.mkyong.crypto.encryptor.EncryptorAesGcmPassword.main(EncryptorAesGcmPassword.java:109)
AES Bestand versleutelen en ontsleutelen.
Dit voorbeeld is een AES wachtwoord-gebaseerde bestands encryptie. De ideeen zijn hetzelfde, maar we hebben wat IO klassen nodig om met de bronnen of bestanden te werken.
Hier is een tekst bestand, in de resources
map.
This is line 1.This is line 2.This is line 3.This is line 4.This is line 5.This is line 9.This is line 10.
4.1 Dit voorbeeld is vergelijkbaar met 3.1 EncryptorAesGcmPassword.java
, met enkele kleine wijzigingen zoals het retourneren van een byte
in plaats van base64 gecodeerde string.
public static byte encrypt(byte pText, String password) throws Exception { //... // prefix IV and Salt to cipher text byte cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + salt.length + cipherText.length) .put(iv) .put(salt) .put(cipherText) .array(); // it works, even if we save the based64 encoded string into a file. // return Base64.getEncoder().encodeToString(cipherTextWithIvSalt); // we save the byte into a file. return cipherTextWithIvSalt; }
Voeg encryptFile
en decryptFile
toe om met het bestand te werken.
public static void encryptFile(String fromFile, String toFile, String password) throws Exception { // read a normal txt file byte fileContent = Files.readAllBytes(Paths.get(ClassLoader.getSystemResource(fromFile).toURI())); // encrypt with a password byte encryptedText = EncryptorAesGcmPasswordFile.encrypt(fileContent, password); // save a file Path path = Paths.get(toFile); Files.write(path, encryptedText); } public static byte decryptFile(String fromEncryptedFile, String password) throws Exception { // read a file byte fileContent = Files.readAllBytes(Paths.get(fromEncryptedFile)); return EncryptorAesGcmPasswordFile.decrypt(fileContent, password); }
4.2 Lees het bovenstaande readme.txt
bestand van het classpath, versleutel het, en de versleutelde gegevens naar een nieuw bestand c:\test\readme.encrypted.txt
.
String password = "password123"; String fromFile = "readme.txt"; // from resources folder String toFile = "c:\\test\\readme.encrypted.txt"; // encrypt file EncryptorAesGcmPasswordFile.encryptFile(fromFile, toFile, password);
Uitvoer
4.3 Lees het versleutelde bestand, ontsleutel het, en druk de uitvoer af.
String password = "password123"; String toFile = "c:\\test\\readme.encrypted.txt"; // decrypt file byte decryptedText = EncryptorAesGcmPasswordFile.decryptFile(toFile, password); String pText = new String(decryptedText, UTF_8); System.out.println(pText);
Uitvoer
This is line 1.This is line 2.This is line 3.This is line 4.This is line 5.This is line 9.This is line 10.
P.S De AES-afbeeldingsversleuteling is hetzelfde concept.
Broncode downloaden
$ git clone https://github.com/mkyong/core-java
$ cd java-crypto
Laat het me weten als het artikel verbetering behoeft. Bedankt.
- Wikipedia – Cipher JavaDoc
- Wikipedia – Cipher Block Chaining (CBC)
- Wikipedia – Galois/Counter Mode (GCM)
- Oracle – KeyGenerator Algorithms JavaDoc
- Java – Hoe genereer ik een willekeurige 12 bytes?
- Waarom zou ik geen ECB-encryptie gebruiken?
- Spring Security Crypto Module
- Wikipedia – PBKDF2
- RFC 8018 – PKCS
- Java – Hoe byte-array samen te voegen en te splitsen
- Java Security Standard Algoritme-namen
- NIST – Aanbeveling voor Galois/Counter Mode (GCM)
mkyong
Oprichter van Mkyong.com, houdt van Java en open source. Volg hem op Twitter. Als mijn tutorials u bevallen, kunt u overwegen een donatie te doen aan deze goede doelen.