PREMESSA

La crittografia a blocchi a chiave simmetrica svolge un ruolo importante nella crittografia dei dati. Questo vuol dire che la stessa chiave viene utilizzata sia per la crittografia che per la decrittografia. Tra quelli esistenti, l'Advanced Encryption Standard (AES) è un algoritmo di crittografia a chiave simmetrica ampiamente utilizzato.

In questo articolo, vedremo come implementare la crittografia e la decrittografia AES utilizzando Java Cryptography Architecture (JCA) all'interno di JDK.

1. L'ALGORITMO AES

L'algoritmo AES è un cifrario a blocchi iterativo a chiave simmetrica che supporta chiavi crittografiche (chiavi segrete) di 128, 192 e 256 bit per crittografare e decrittografare i dati in blocchi di 128 bit. La figura seguente mostra l'algoritmo AES di alto livello:

Se i dati da crittografare non soddisfano i requisiti della dimensione del blocco di 128 bit, è necessario riempirli, mediante un processo di riempimento dell'ultimo blocco a 128 bit.

2. VARIANTI AES

L'algoritmo AES ha sei modalità di funzionamento:

  1. ECB (Electronic Code Booke)

  2. CBC (Cipher Block Chaining)

  3. CFB (Cipher FeedBack)

  4. OFB (Output FeedBack)

  5. CTR (Contatore)

  6. GCM (Galois / Counter Mode)

La modalità di funzionamento può essere applicata per rafforzare l'effetto dell'algoritmo di crittografia; inoltre, può convertire il cifrario a blocchi in un cifrario a flusso. Ogni modalità ha i suoi punti di forza e di debolezza. Facciamo una rapida disamina dei principali.

2.1 ECB

Questa modalità di funzionamento è la più semplice di tutte. Il testo in chiaro è suddiviso in blocchi con una dimensione di 128 bit. Quindi ogni blocco verrà crittografato con la stessa chiave e algoritmo. Pertando, produce lo stesso risultato per lo stesso blocco. Questo è il principale punto debole di questa modalità e non è consigliato per la crittografia. Richiede dati di riempimento.

2.2. CBC

Per superare la debolezza della ECB, la modalità CBC impiega un Initialization Vecto (IV) atto ad aumentare la crittografia. Innanzitutto, CBC utilizza il blocco di testo in chiaro xor con IV; quindi crittografia il risultato nel blocco di testo cifrato. Nel blocco successivo, utilizza il risultato della crittografia in xor con il blocco di testo in chiaro fino all'ultimo blocco.

In questa modalità, la crittografia non può essere parallelizzata, a differenza della decrittografia. Richiede anche dati di riempimento.

2.3 CFB

Questa modalità può essere utilizzata come cifrario a flusso. In primo luogo, crittografa l'IV, quindi si applica l'operatore xor con il blocco di testo in chiaro per ottenere il testo cifrato. CFB crittografa il risultato della crittografia in xor con il testo in chiaro. Si ha necessità di un Initialization Vector (IV).

In questa modalità, la decrittografia può essere parallelizzata a differenza della crittografia.

2.4 OFB

Questa modalità può essere utilizzata come cifrario a flusso. Innanzitutto, crittografa l'IV. Quindi utilizza i risultati della crittografia per applicare l'operatore xor sul testo in chiaro per ottenere quello cifrato.

Non richiede dati di riempimento e non sarà influenzato dal blocco rumoroso.

2.5 CTR

Questa modalità utilizza il valore di un contatore come IV. E' molto simile a OFB, ma utilizza il contatore per essere crittografato ogni volta invece dell'IV. Tale sistema ha due punti di forza: la parallelizzazione di crittografia/decrittografia e il rumore in un blocco non influisce sugli altri blocchi.

2.6 GCM

Questa modalità è un'estenzione della CTR. Il GCM ha ricevuto una notevole attenzione ed è raccomandato dal NIST; restituisce un testo cifrato e un tag di autenticazione. Il vantaggio principale di questa modalità, rispetto ad altre operative dell'algoritmo, è la sua efficienza.

In questo articolo faremo riferimento all'algoritmo AES/CBC/PKCS5Padding, in quanto ampiamente utilizzato in molti progetti.

DIMENSIONI DEI DATI DOPO LA CRITTOGRAFIA

Come accennato in precedenza, l'AES ha una dimensione del blocco di 128 bit o 16 byte. Esso non cambia la dimensione e quella del testo cifrato è uguale alla dimensione del testo in chiaro. Inoltre, nelle modalità ECB e CBC, dovremmo utilizzare un algoritmo di riempimento come PKCS 5. Quindi, la dimensione dei dati dopo la crittografia è:

ciphertext_size (bytes) = cleartext_size + (16 - (cleartext_size % 16))

Per memorizzare IV con testo cifrato, dobbiamo aggiungere ulteriori 16 byte.

3. PARAMETRI AES

Nell'algoritmo AES, abbiamo bisogno di tre parametri: dati di input, chiave segreta e IV. IV non è utilizzato in modalità ECB.

3.1 DATI IN INGRESSO

I dati di input in AES possono essere basati su stringhe, file, oggetti e password.

3.2 CHIAVE SEGRETA

Ci sono due modi per generare una chiave segreta nell'AES: da un numero casuale o derivando da una data password.

Nel primo approccio, la chiave segreta dovrebbe essere generata da un generatore di numeri casuali crittograficamente sicuro (pseudo-) come la classe SecureRandom.

Per generare una chiave segreta, possiamo usare la classe KeyGenerator. Definiamo un metodo per generare la chiave AES con la dimensione di n (128, 192 e 256) bit:

public static SecretKey generateKey(int n) throws NoSuchAlgorithmException {

    KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");

    keyGenerator.init(n);

    SecretKey key = keyGenerator.generateKey();

    return key;

}

Nel secondo approccio, la chiave segreta AES può essere derivata da una determinata password utilizzando una funzione di derivazione della chiave basata su password come PBKDF2. Abbiamo anche bisogno di un valore salt (una stringa alfanumerica) per trasformare una password in una chiave segreta. Anche il salt è un valore casuale.

Possiamo usare la classe SecretKeyFactory con l'algoritmo PBKDF2WithHmacSHA256 per generare una chiave da una data password.

Definiamo un metodo per generare la chiave AES da una data password con 65.536 iterazioni e una lunghezza della chiave di 256 bit:

public static SecretKey getKeyFromPassword(String password, String salt)

    throws NoSuchAlgorithmException, InvalidKeySpecException {

    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");

    KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);

    SecretKey secret = new SecretKeySpec(factory.generateSecret(spec)

        .getEncoded(), "AES");

    return secret;

}

3.3 VETTORE DI INIZIALIZZAZIONE (IV)

IV è un valore pseudocasuale e ha le stesse dimensioni del blocco crittografato. Possiamo usare la classe SecureRandom per generare un IV casuale.

Definiamo un metodo per generare un IV:

public static IvParameterSpec generateIv() {

    byte[] iv = new byte[16];

    new SecureRandom().nextBytes(iv);

    return new IvParameterSpec(iv);

}

4. CRITTOGRAFIA E DECRITTOGRAFIA

4.1 STRINGHE DI TESTO

Per implementare la crittografia della stringa di input, dobbiamo prima generare la chiave segreta e IV secondo la sezione precedente. Come passaggio successivo, creiamo un'istanza dalla classe Cipher utilizzando il metodo getInstance ().

Inoltre, configuriamo un'istanza di crittografia utilizzando il metodo init () con una chiave segreta, IV e modalità di crittografia. Infine, crittografiamo la stringa di input invocando il metodo doFinal (). Questo metodo ottiene byte di input e restituisce il testo cifrato in byte:

public static String encrypt(String algorithm, String input, SecretKey key,

    IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,

    InvalidAlgorithmParameterException, InvalidKeyException,

    BadPaddingException, IllegalBlockSizeException {

    Cipher cipher = Cipher.getInstance(algorithm);

    cipher.init(Cipher.ENCRYPT_MODE, key, iv);

    byte[] cipherText = cipher.doFinal(input.getBytes());

    return Base64.getEncoder()

        .encodeToString(cipherText);

}

Per decrittografare una stringa di input, possiamo inizializzare il nostro codice utilizzando DECRYPT_MODE:

public static String decrypt(String algorithm, String cipherText, SecretKey key,

    IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,

    InvalidAlgorithmParameterException, InvalidKeyException,

    BadPaddingException, IllegalBlockSizeException {

    Cipher cipher = Cipher.getInstance(algorithm);

    cipher.init(Cipher.DECRYPT_MODE, key, iv);

    byte[] plainText = cipher.doFinal(Base64.getDecoder()

        .decode(cipherText));

    return new String(plainText);

}

Scriviamo un metodo di prova per crittografare e decrittografare un input di stringa:

@Test

void givenString_whenEncrypt_thenSuccess()

    throws NoSuchAlgorithmException, IllegalBlockSizeException, InvalidKeyException,

    BadPaddingException, InvalidAlgorithmParameterException, NoSuchPaddingException {

    String input = "dreams";

    SecretKey key = AESUtil.generateKey(128);

    IvParameterSpec ivParameterSpec = AESUtil.generateIv();

    String algorithm = "AES/CBC/PKCS5Padding";

    String cipherText = AESUtil.encrypt(algorithm, input, key, ivParameterSpec);

    String plainText = AESUtil.decrypt(algorithm, cipherText, key, ivParameterSpec);

    Assertions.assertEquals(input, plainText);

}

4.2 FILE

Ora crittografiamo un file utilizzando l'algoritmo AES. I passaggi sono gli stessi, ma abbiamo bisogno di alcune classi IO per lavorare con i file. Crittografiamo un file di testo:

public static void encryptFile(String algorithm, SecretKey key, IvParameterSpec iv,

    File inputFile, File outputFile) throws IOException, NoSuchPaddingException,

    NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException,

    BadPaddingException, IllegalBlockSizeException {

    Cipher cipher = Cipher.getInstance(algorithm);

    cipher.init(Cipher.ENCRYPT_MODE, key, iv);

    FileInputStream inputStream = new FileInputStream(inputFile);

    FileOutputStream outputStream = new FileOutputStream(outputFile);

    byte[] buffer = new byte[64];

    int bytesRead;

    while ((bytesRead = inputStream.read(buffer)) != -1) {

        byte[] output = cipher.update(buffer, 0, bytesRead);

        if (output != null) {

            outputStream.write(output);

        }

    }

    byte[] outputBytes = cipher.doFinal();

    if (outputBytes != null) {

        outputStream.write(outputBytes);

    }

    inputStream.close();

    outputStream.close();

}

Si noti che si sconsiglia di provare a leggere l'intero file, in particolare se è di grandi dimensioni, in memoria. Invece, crittografiamo un buffer alla volta.

Per decrittografare un file, utilizziamo passaggi simili e inizializziamo il nostro codice utilizzando DECRYPT_MODE come abbiamo visto prima.

Ancora una volta, definiamo un metodo di prova per crittografare e decrittografare un file di testo. In questo metodo, leggiamo il file wonderlab.txt dalla directory delle risorse di test, lo crittografiamo in un file chiamato wonderlab.encrypted, quindi decifriamo il file in un nuovo file:

@Test

void givenFile_whenEncrypt_thenSuccess()

    throws NoSuchAlgorithmException, IOException, IllegalBlockSizeException,

    InvalidKeyException, BadPaddingException, InvalidAlgorithmParameterException,

    NoSuchPaddingException {

    SecretKey key = AESUtil.generateKey(128);

    String algorithm = "AES/CBC/PKCS5Padding";

    IvParameterSpec ivParameterSpec = AESUtil.generateIv();

    Resource resource = new ClassPathResource("inputFile/dreams.txt");

    File inputFile = resource.getFile();

    File encryptedFile = new File("classpath:dreams.encrypted");

    File decryptedFile = new File("document.decrypted");

    AESUtil.encryptFile(algorithm, key, ivParameterSpec, inputFile, encryptedFile);

    AESUtil.decryptFile(

      algorithm, key, ivParameterSpec, encryptedFile, decryptedFile);

    assertThat(inputFile).hasSameTextualContentAs(decryptedFile);

}

4.3 UTILIZZO DI PASSWORD

Possiamo eseguire la crittografia e la decrittografia AES utilizzando la chiave segreta derivata da una determinata password.

Per generare una chiave segreta, utilizziamo il metodo getKeyFromPassword (). I passaggi di crittografia e decrittografia sono gli stessi di quelli mostrati nella sezione di input della stringa. Possiamo quindi utilizzare la crittografia istanziata e la chiave segreta fornita per eseguire la crittografia.

Scriviamo un metodo di prova:

@Test

void givenPassword_whenEncrypt_thenSuccess()

    throws InvalidKeySpecException, NoSuchAlgorithmException,

    IllegalBlockSizeException, InvalidKeyException, BadPaddingException,

    InvalidAlgorithmParameterException, NoSuchPaddingException {

    String plainText = "dreams.news";

    String password = "dreams";

    String salt = "12345678";

    IvParameterSpec ivParameterSpec = AESUtil.generateIv();

    SecretKey key = AESUtil.getKeyFromPassword(password,salt);

    String cipherText = AESUtil.encryptPasswordBased(plainText, key, ivParameterSpec);

    String decryptedCipherText = AESUtil.decryptPasswordBased(

      cipherText, key, ivParameterSpec);

    Assertions.assertEquals(plainText, decryptedCipherText);

}

4.4 OBJECT

Per crittografare un oggetto Java, dobbiamo utilizzare la classe SealedObject. L'oggetto dovrebbe essere serializzabile. Cominciamo definendo una classe Student:

public class Student implements Serializable {

    private String name;

    private int age;

    // standard setters and getters

}

Dunque, crittografiamo l'oggetto Student:

public static SealedObject encryptObject(String algorithm, Serializable object,

    SecretKey key, IvParameterSpec iv) throws NoSuchPaddingException,

    NoSuchAlgorithmException, InvalidAlgorithmParameterException,

    InvalidKeyException, IOException, IllegalBlockSizeException {

    Cipher cipher = Cipher.getInstance(algorithm);

    cipher.init(Cipher.ENCRYPT_MODE, key, iv);

    SealedObject sealedObject = new SealedObject(object, cipher);

    return sealedObject;

}

L'oggetto crittografato può essere successivamente decrittografato utilizzando la crittografia corretta:

public static Serializable decryptObject(String algorithm, SealedObject sealedObject,

    SecretKey key, IvParameterSpec iv) throws NoSuchPaddingException,

    NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException,

    ClassNotFoundException, BadPaddingException, IllegalBlockSizeException,

    IOException {

    Cipher cipher = Cipher.getInstance(algorithm);

    cipher.init(Cipher.DECRYPT_MODE, key, iv);

    Serializable unsealObject = (Serializable) sealedObject.getObject(cipher);

    return unsealObject;

}

Scriviamo un test case:

@Test

void givenObject_whenEncrypt_thenSuccess()

    throws NoSuchAlgorithmException, IllegalBlockSizeException, InvalidKeyException,

    InvalidAlgorithmParameterException, NoSuchPaddingException, IOException,

    BadPaddingException, ClassNotFoundException {

    Student student = new Student("Dreams", 20);

    SecretKey key = AESUtil.generateKey(128);

    IvParameterSpec ivParameterSpec = AESUtil.generateIv();

    String algorithm = "AES/CBC/PKCS5Padding";

    SealedObject sealedObject = AESUtil.encryptObject(

      algorithm, student, key, ivParameterSpec);

    Student object = (Student) AESUtil.decryptObject(

      algorithm, sealedObject, key, ivParameterSpec);

    assertThat(student).isEqualToComparingFieldByField(object);

}

CONCLUSIONE

In sintesi, abbiamo imparato come cifrare e decifrare dei dati di input, come delle stringhe di testo, dei file, degli oggetti Java e dei dati basati su password, usando l’algoritmo simmetrico AES in Java. Inoltre abbiamo discusso le varianti AES e la dimensione dei dati ottenuta dopo la loro crittografia.

Articoli Recenti