Crittografia e decrittografia Java AES

immagine dell'autore
By mkyong | Ultimo aggiornamento: 2 giugno 2020
Visitato: 64,491 | +1,553 pv/w

Crittografia AES

L’Advanced Encryption Standard (AES, Rijndael) è un algoritmo di crittografia e decrittografia a blocchi, l’algoritmo di crittografia più usato nel mondo. L’AES elabora blocchi di 128 bit usando una chiave segreta di 128, 192 o 256 bit.

Questo articolo mostra alcuni esempi di crittografia e decrittografia Java AES:

  • Crittografia di stringhe AES – (crittografa e decritta una stringa).
  • Crittografia basata su password AES – (la chiave segreta deriva da una data password).
  • Crittografia di file AES. (basata su password).

In questo articolo, ci concentriamo sulla crittografia AES a 256 bit con Galois Counter Mode (GCM).

GCM = CTR + Authentication.

Altre letture
Leggi questo – NIST – Raccomandazione per il Galois/Counter Mode (GCM)

Non usare AES Electronic codebook (ECB) Mode
La modalità AES ECB, o AES/ECB/PKCS5Padding (in Java) non è semanticamente sicura – Il testo cifrato con ECB può far trapelare informazioni sul testo in chiaro. Qui c’è una discussione su Perché non dovrei usare la crittografia ECB?

Java e gli input della crittografia AES.

Nella crittografia e decrittografia AES, abbiamo bisogno dei seguenti input:

Buona pratica di crittografia AES
Non riutilizzare IV con la stessa chiave.

1.1 Il IV (valore iniziale o vettore iniziale), è un byte casuale, in genere 12 byte o 16 byte. In Java, possiamo usare SecureRandom per generare l’IV casuale.

 // 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 La chiave segreta AES, o AES-128 o AES-256. In Java, possiamo usare KeyGenerator per generare la chiave segreta AES.

 // 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 La chiave segreta AES che deriva da una data password. In Java, possiamo usare il SecretKeyFactory e PBKDF2WithHmacSHA256 per generare una chiave AES da una data password.

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

Usiamo salt per proteggere gli attacchi rainbow, ed è anche un byte casuale, possiamo usare lo stesso 1.1 getRandomNonce per generarlo.

1.4 Raggruppiamo i metodi di cui sopra in una singola classe util, in modo da non ripetere sempre lo stesso codice.

CryptoUtils.java
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(); }}

Codifica e decodifica AES.

L’AES-GSM è il cifratore autenticato più usato. Questo esempio cifrerà e decifrerà una stringa usando AES a 256 bit in modalità Galois Counter Mode (GCM).

Gli input di AES-GCM:

  • chiave segreta AES (256 bit)
  • IV – 96 bit (12 byte)
  • Lunghezza (in bit) del tag di autenticazione – 128 bit (16 byte)

2.1 In Java, usiamo AES/GCM/NoPadding per rappresentare l’algoritmo AES-GCM. Per l’output criptato, mettiamo un prefisso di 16 byte IV al testo criptato (ciphertext), perché abbiamo bisogno dello stesso IV per la decriptazione.

Questo va bene se IV è pubblicamente noto?
Va bene che l’IV sia pubblicamente noto, l’unico segreto è la chiave, tienila privata e confidenziale.

Questo esempio userà AES per cifrare un testo in chiaro Hello World AES-GCM e poi decifrarlo di nuovo nel testo in chiaro originale.

EncryptorAesGcm.java
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)); }}

Output

Testo in chiaro: Hello World AES-GCM

Terminale
------ 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

Testo in chiaro: Hello World AES-GCM, Welcome to Cryptography!

Terminal
------ 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!

Crittografia e decrittografia basata su password AES.

Per la crittografia basata su password, possiamo usare la specifica di crittografia basata su password (PKCS), definita RFC 8018, per generare una chiave da una data password.

Per gli input PKCS:

  • Password, si fornisce questo.
  • Sale – Almeno 64 bit (8 byte) casuali.
  • Conteggio delle iterazioni – Raccomanda un conteggio minimo di 1.000 iterazioni.

Cosa sono il sale e il conteggio delle iterazioni?

  • Il salt produce un ampio set di chiavi per una data password. Per esempio, se il sale è di 128 bit, ci saranno ben 2^128 chiavi per ogni password. Pertanto, aumenta la difficoltà degli attacchi arcobaleno. Inoltre, la tabella arcobaleno che gli attaccanti costruiscono per la password di un utente diventa inutile per un altro utente.
  • Il iteration count aumentando il costo di produzione delle chiavi da una password, quindi aumenta la difficoltà e rallenta la velocità degli attacchi.

3.1 Per l’output criptato, prefissiamo il 12 bytes IV e password salt al testo cifrato, perché abbiamo bisogno dello stesso IV e del sale della password (per la chiave segreta) per la decrittazione. Inoltre, usiamo Base64 encoder per codificare il testo criptato in una rappresentazione stringa, in modo da poter inviare il testo criptato o il testo cifrato in formato stringa (era un array di byte).

Va bene se il sale della password è pubblicamente noto?
È lo stesso per il IV, e va bene che il sale della password sia pubblicamente noto, l’unico segreto è la chiave, mantenendola privata e riservata.

EncryptorAesGcmPassword.java
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

Terminal
------ 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 Se la password non corrisponde, Java lancia 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

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

Codifica e decodifica file AES.

Questo esempio è una crittografia di file basata su password AES. Le idee sono le stesse, ma abbiamo bisogno di alcune classi IO per lavorare con le risorse o i file.

Ecco un file di testo, nella cartella resources.

readme.txt
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 Questo esempio è simile a 3.1 EncryptorAesGcmPassword.java, con alcuni cambiamenti minori come restituire un byte invece della stringa codificata in base64.

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

Aggiungere encryptFile e decryptFile per lavorare con il file.

 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 Leggere il file readme.txt di cui sopra dal classpath, crittografarlo, e i dati crittografati in un nuovo file 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);

Output

Crittografia file AES

4.3 Leggere il file crittografato, decifrarlo e stampare l’output.

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

Output

Terminale
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 La crittografia delle immagini AES è lo stesso concetto.

Scaricare il codice sorgente

$ git clone https://github.com/mkyong/core-java

$ cd java-crypto

Fatemi sapere se l’articolo ha bisogno di miglioramenti. Grazie.

  • Wikipedia – Cipher JavaDoc
  • Wikipedia – Cipher Block Chaining (CBC)
  • Wikipedia – Galois/Counter Mode (GCM)
  • Oracle – KeyGenerator Algorithms JavaDoc
  • Java – Come generare un 12 bytes casuale?
  • Perché non dovrei usare la crittografia ECB?
  • Spring Security Crypto Module
  • Wikipedia – PBKDF2
  • RFC 8018 – PKCS
  • Java – Come unire e dividere array di byte
  • Java Security Standard Nomi degli algoritmi
  • NIST – Raccomandazione per Galois/Counter Mode (GCM)
immagine autore

mkyong

Fondatore di Mkyong.com, ama Java e le cose open source. Seguilo su Twitter. Se ti piacciono i miei tutorial, considera di fare una donazione a questi enti di beneficenza.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *