Categories
Development Java

Java Encryption and Decryption

Für ein Projekt soll ich einen QR-Code generieren, der eine URL beinhaltet, die den Zugriff auf eine bestimmte Ressource ermöglicht.

Das einfachste Beispiel wäre so etwas wie: https://Kaulbach.de/QR-Ressouce/id=1

Ich möchte allerdings nicht, dass jemand, der die URL kennt, alle Ressourcen ansehen kann, indem er einfach den id-Parameter hochzählt.

Daher soll der id-Parameter verschlüsselt werden.

Tutorial

Ich arbeite mich durch den Artikel Java AES Encryption and Decryption auf Baeldung.

Verwendet wird Advanced Encryption Standard (AES) in der Variante AES/CBC/PKCS5Padding "because it's widely used in many projects".

AES Parameters

In the AES algorithm, we need three parameters: input data, secret key, and IV

Im Code wird dann auch noch ein Salt verwendet, also sind es sogar vier Parameter.

Code

Der komplette Code des Tutorials befindet sich in GitHub.

Hier der Minimal-Code, der für mich relevant ist:

package deringo;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public class AESUtil {

    public static void main(String[] args) throws Exception {
        String input = "baeldung";
        String password = "myPassword";
        String salt = "mySalt";
        SecretKey key = getKeyFromPassword(password, salt); //generateKey(128);
        IvParameterSpec ivParameterSpec = generateIv();
        String algorithm = "AES/CBC/PKCS5Padding";
        String cipherText = encrypt(algorithm, input, key, ivParameterSpec);
        String plainText = decrypt(algorithm, cipherText, key, ivParameterSpec);
        System.out.println( input + " : " + plainText);
    }
    
    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;
    }
    
    public static IvParameterSpec generateIv() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return new IvParameterSpec(iv);
    }
    
    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);
    }
    
    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);
    }

}

Allerdings ändert sich bei jedem Durchlauf der Initialization Vector (IV). Dadurch kann man den verschlüsselten Wert (der in dem Beispielcode nicht ausgegeben wird) nicht in einem zweiten Durchlauf wieder entschlüsseln. Das entspricht somit noch nicht dem, was ich brauche.

Mein Code

Ich möchte lediglich einen String verschlüsseln, über den QR-Code weitergeben und später soll der verschlüsselte Wert wieder entschlüsselt werden. Daher muss ich, anders als in dem Beispiel oben, Passwort, Salt und IV speichern.

Dazu brauche ich einen IV im String Format, den ich mir als erstes generieren lasse:

public static String generateIV() {
  byte[] iv = new byte[16];
  new SecureRandom().nextBytes(iv);
  return Base64.getEncoder().encodeToString(iv);
}

Passwort, Salt und IV-String werden in den Properties gespeichert:

##################################### 
### SimpleCryptoUtil
SimpleCryptoUtil.password=myPassword
SimpleCryptoUtil.salt=mySalt
SimpleCryptoUtil.iv=2WbRy1wh3HsLe8Vir3TxkA==
#####################################

Damit kann ich die Methoden zum Erstellen von SecretKey und IvParameterSpec erstellen und darauf aufbauend die Methoden encrypt(String) und decrypt(String):

package deringo;

import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.quinscape.melba.support.Config;

public class SimpleCryptoUtil {
    private static Logger logger = LoggerFactory.getLogger(SimpleCryptoUtil.class);
    
    public static String encrypt(String input) {
        try {
            String algorithm = "AES/CBC/PKCS5Padding";
            Cipher cipher = Cipher.getInstance(algorithm);
            cipher.init(Cipher.ENCRYPT_MODE, getKey(), getIv());
            byte[] cipherText = cipher.doFinal(input.getBytes());
            return Base64.getEncoder().encodeToString(cipherText);
        } catch (GeneralSecurityException e) {
            logger.error(e.getMessage());
            return null;
        }
    }
    
    public static String decrypt(String input) {
        try {
            String algorithm = "AES/CBC/PKCS5Padding";
            Cipher cipher = Cipher.getInstance(algorithm);
            cipher.init(Cipher.DECRYPT_MODE, getKey(), getIv());
            byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(input));
            return new String(plainText);
        } catch (GeneralSecurityException e) {
            logger.error(e.getMessage());
            return null;
        }
    }
    
    private static SecretKey getKey() throws InvalidKeySpecException, NoSuchAlgorithmException {
        String password = Config.get("SimpleCryptoUtil.password");
        String salt     = Config.get("SimpleCryptoUtil.salt");
        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;
    }
    
    private static IvParameterSpec getIv() {
        String iv = Config.get("SimpleCryptoUtil.iv");
        return new IvParameterSpec(Base64.getDecoder().decode(iv));
    }

    protected static String generateIV() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return Base64.getEncoder().encodeToString(iv);
    }
}

Die Verwendung ist dann so einfach wie gewünscht:

public static void main(String[] args) throws Exception {
  String secret = "geheim";
  String verschluesselt = SimpleCryptoUtil.encrypt(secret);
  System.out.println(secret + " : " + verschluesselt);
  String entschluesselt = SimpleCryptoUtil.decrypt(verschluesselt);
  System.out.println(verschluesselt + " : " + entschluesselt);
}