Search code examples
javaencryptionblowfish

Java Blowfish Encryption with CBC


I'm trying to produce what this site is doing https://codebeautify.org/encrypt-decrypt using Blowfish and CBC

I'm not sure what's the actual term but the encryption method that I'd like to achieve shall produce non-consistent encrypted string despite using same content and key,

For example if I encrypt Hello with key key123, twice, the first result may show abcde, and second should show something else, like fghij. But decrypting both abcde and fghij with key123 shall return the same Hello.

enter image description here

Also may I know what's the type of encoding they are using to produce the final result? Such as hex/base64, because I tried both, but it doesn't seems to produce similar result.

This is what I'm using:

The Crypto class:

public static String enc(String content, String key) {
    String encCon = "";

    try {
        String IV = "12345678";

        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes("UTF-8"), "Blowfish");
        Cipher cipher = Cipher.getInstance("Blowfish/CBC/PKCS5Padding");

        String secret = content;
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, new javax.crypto.spec.IvParameterSpec(IV.getBytes("UTF-8")));
        byte[] encoding = cipher.doFinal(secret.getBytes("UTF-8"));

        System.out.println("-- Encrypted -----------");
        encCon = DatatypeConverter.printBase64Binary(encoding);
        System.out.println("-- encCon : " + encCon);
    } catch (Exception ex) {
        logger.error(ex.getMessage(), ex);
    }

    return encCon;
}

public static String dec(String content, String key) {
    String decCon = "";

    try {
        String IV = "12345678";

        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes("UTF-8"), "Blowfish");
        Cipher cipher = Cipher.getInstance("Blowfish/CBC/PKCS5Padding");

        // Decode Base64
        byte[] ciphertext = DatatypeConverter.parseBase64Binary(content);

        // Decrypt
        cipher.init(Cipher.DECRYPT_MODE, keySpec, new javax.crypto.spec.IvParameterSpec(IV.getBytes("UTF-8")));
        byte[] message = cipher.doFinal(ciphertext);

        System.out.println("-- Decrypted -----------");
        decCon = new String(message, "UTF-8");
        System.out.println("-- decCon : " + decCon);
    } catch (Exception ex) {
        logger.error(ex.getMessage(), ex);
    }

    return decCon;
}

The calling class (such as Main.java)

// This is what I get from codebeautify site, encrypting Hello with key123
// However, I'm getting javax.crypto.BadPaddingException: Given final block not properly padded
Crypto.dec("08GCpwyZc+qGNuxSvXAD2A==", "key123"); 

// Below 2 lines works fine, the only problem is the result isn't randomized
String encContent = Crypto.enc("Hello", "key123");
Crypto.dec(encContent, "key123");

Solution

  • UPDATE 2019-04-21 09:49 P.M. UTC

    After @MaartenBodewes and @MarkJeronimus have pointed out some things to consider, I am updating the answer to make it more correct. But because this question is about the implementation, not about making it more secure, this and old version should be sufficient for at least giving a little insight. Again, more secure solution can be achieved by modifying below code.

    Changelog

    • Key Derivation
    • Handling Exceptions with their details
    • Using single SecureRandom instance for each data (iv[8 bytes] and salt[32 bytes])
    • Check for null value and emptiness for plaintext to be encrypted and the encrypted text to be decrypted
    import javax.crypto.*;
    import javax.crypto.spec.SecretKeySpec;
    import java.io.UnsupportedEncodingException;
    import java.security.InvalidAlgorithmParameterException;
    import java.security.InvalidKeyException;
    import java.security.NoSuchAlgorithmException;
    import java.security.spec.InvalidKeySpecException;
    import java.util.Base64;
    import javax.xml.bind.DatatypeConverter;
    import java.security.SecureRandom;
    import java.security.spec.KeySpec;
    import javax.crypto.spec.IvParameterSpec;
    import javax.crypto.spec.PBEKeySpec;
    
    public class Crypto {
        private static final char[] tempKey = new char[] {'T', 'E', 'M', 'P', '_', 'G', 'E', 'N', '_', 'K', 'E', 'Y'};
        private static final SecureRandom secureRandomForSalt = new SecureRandom();
        private static final SecureRandom secureRandomForIV = new SecureRandom();
    
        private static byte[] generateSalt() throws RuntimeException {
            try{
                byte[] saltBytes = new byte[32];
    
                secureRandomForSalt.nextBytes(saltBytes);
    
                return saltBytes;
            }
            catch(Exception ex){
                ex.printStackTrace();
                throw new RuntimeException("An error occurred in salt generation part. Reason: " + ex.getMessage());
            }
        }
    
        public static String enc(String content) throws RuntimeException {
            String encClassMethodNameForLogging = Crypto.class.getName() + ".enc" + " || ";
    
            byte[] salt;
            byte[] encodedTmpSecretKey;
            SecretKeySpec keySpec;
            Cipher cipher;
            byte[] iv;
            IvParameterSpec ivParameterSpec;
            String finalEncResult;
    
            if(content == null || content.trim().length() == 0) {
                throw new RuntimeException("To be encrypted text is null or empty");
            }
    
            System.out.println("-- Encrypting -----------");
    
            try {
                salt = generateSalt();
            }
            catch (Exception ex) {
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in salt generation part. Reason: " + ex.getMessage());
            }
    
            try {
                SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
                KeySpec spec = new PBEKeySpec(Crypto.tempKey, salt, 65536, 256);
                SecretKey tmpSecretKey = factory.generateSecret(spec);
    
                encodedTmpSecretKey = tmpSecretKey.getEncoded();
                System.out.println("-- Secret Key Derivation in Encryption: " + Base64.getEncoder().encodeToString(encodedTmpSecretKey));
            }
            catch (NoSuchAlgorithmException ex){
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in key derivation part. Reason: " + ex.getMessage() + " - Explanation: The particular cryptographic algorithm requested is not available in the environment");
            }
            catch (InvalidKeySpecException ex){
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in key derivation part. Reason: " + ex.getMessage() + " - Explanation: Key length may not be correct");
            }
            catch (Exception ex){
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in key derivation part. Reason: " + ex.getMessage());
            }
    
            try {
                keySpec = new SecretKeySpec(encodedTmpSecretKey, "Blowfish");
                cipher = Cipher.getInstance("Blowfish/CBC/PKCS5Padding");
            }
            catch (NoSuchAlgorithmException ex){
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in cipher instantiation part. Reason: " + ex.getMessage() + " - Explanation: The particular cryptographic algorithm requested is not available in the environment");
            }
            catch (NoSuchPaddingException ex){
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in cipher instantiation part. Reason: " + ex.getMessage() + " - Explanation: The particular padding mechanism is requested but is not available in the environment");
            }
            catch (Exception ex){
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in cipher instantiation part. Reason: " + ex.getMessage());
            }
    
            try {
                iv = new byte[cipher.getBlockSize()];
                secureRandomForIV.nextBytes(iv);
                ivParameterSpec = new IvParameterSpec(iv);
            }
            catch (Exception ex){
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in iv creation part. Reason: " + ex.getMessage());
            }
    
            try {
                cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivParameterSpec);
                byte[] encoding = cipher.doFinal(content.getBytes("UTF-8"));
    
                String encCon = DatatypeConverter.printBase64Binary(encoding);
                String ivStr = DatatypeConverter.printBase64Binary(iv);
                String saltStr = DatatypeConverter.printBase64Binary(salt);
    
                System.out.println("-- encCon : " + encCon);
                System.out.println("-- iv : " + ivStr);
                System.out.println("-- salt : " + saltStr);
    
                finalEncResult = encCon + ":" + ivStr + ":" + saltStr;
                System.out.println("-- finalEncRes : " + finalEncResult + "\n");
            }
            catch (InvalidKeyException ex){
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in encryption part. Reason: " + ex.getMessage() + " - Explanation: Most probably you didn't download and copy 'Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files'");
            }
            catch (InvalidAlgorithmParameterException ex){
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in decryption part. Reason: " + ex.getMessage() + " - Explanation: IV length may not be correct");
            }
            catch (IllegalBlockSizeException ex){
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in decryption part. Reason: " + ex.getMessage() + " - Explanation: The length of data provided to a block cipher is incorrect, i.e., does not match the block size of the cipher");
            }
            catch (BadPaddingException ex){
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in encryption part. Reason: " + ex.getMessage() + " - Explanation: A particular padding mechanism is expected for the input data but the data is not padded properly (Most probably wrong/corrupt key caused this)");
            }
            catch (UnsupportedEncodingException ex){
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in encryption part. Reason: " + ex.getMessage() + " - Explanation: The Character Encoding is not supported");
            }
            catch (Exception ex){
                ex.printStackTrace();
                throw new RuntimeException(encClassMethodNameForLogging + "An error occurred in encryption part. Reason: " + ex.getMessage());
            }
    
            return finalEncResult;
        }
    
        public static String dec(String encContent) throws RuntimeException {
            String decClassMethodNameForLogging = Crypto.class.getName() + ".dec" + " || ";
    
            String decCon;
            byte[] salt;
            byte[] encodedTmpSecretKey;
            SecretKeySpec keySpec;
            Cipher cipher;
            byte[] iv;
    
            if(encContent == null || encContent.trim().length() == 0) {
                throw new RuntimeException("To be decrypted text is null or empty");
            }
    
            System.out.println("-- Decrypting -----------");
    
            try {
                salt = DatatypeConverter.parseBase64Binary(encContent.substring(encContent.lastIndexOf(":") + 1));
            }
            catch (Exception ex) {
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in salt retrieving part. Reason: " + ex.getMessage());
            }
    
            try {
                SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
                KeySpec spec = new PBEKeySpec(Crypto.tempKey, salt, 65536, 256);
                SecretKey tmpSecretKey = factory.generateSecret(spec);
    
                encodedTmpSecretKey = tmpSecretKey.getEncoded();
                System.out.println("-- Secret Key Gathering in Decryption: " + Base64.getEncoder().encodeToString(encodedTmpSecretKey));
            }
            catch (NoSuchAlgorithmException ex){
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in key derivation part. Reason: " + ex.getMessage() + " - Explanation: The particular cryptographic algorithm requested is not available in the environment");
            }
            catch (InvalidKeySpecException ex){
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in key derivation part. Reason: " + ex.getMessage() + " - Explanation: Key length may not be correct");
            }
            catch (Exception ex) {
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in key derivation part. Reason: " + ex.getMessage());
            }
    
            try {
                keySpec = new SecretKeySpec(encodedTmpSecretKey, "Blowfish");
                cipher = Cipher.getInstance("Blowfish/CBC/PKCS5Padding");
            }
            catch (NoSuchAlgorithmException ex){
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in cipher instantiation part. Reason: " + ex.getMessage() + " - Explanation: The particular cryptographic algorithm requested is not available in the environment");
            }
            catch (NoSuchPaddingException ex){
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in cipher instantiation part. Reason: " + ex.getMessage() + " - Explanation : The particular padding mechanism requested is not available in the environment");
            }
            catch (Exception ex) {
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in cipher instantiation part. Reason: " + ex.getMessage());
            }
    
            try {
                iv = DatatypeConverter.parseBase64Binary(encContent.substring(encContent.indexOf(":") + 1, encContent.lastIndexOf(":")));
            }
            catch (Exception ex) {
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in iv creation part. Reason: " + ex.getMessage());
            }
    
            try {
                cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv));
                byte[] decoding = cipher.doFinal(Base64.getDecoder().decode(encContent.substring(0, encContent.indexOf(":"))));
    
                decCon = new String(decoding, "UTF-8");
                System.out.println("-- decCon : " + decCon + "\n");
            }
            catch (InvalidKeyException ex){
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in decryption part. Reason: " + ex.getMessage() + " - Explanation: Most probably you didn't download and copy 'Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files'");
            }
            catch (InvalidAlgorithmParameterException ex){
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in decryption part. Reason: " + ex.getMessage() + " - Explanation: IV length may not be correct");
            }
            catch (IllegalBlockSizeException ex){
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in decryption part. Reason: " + ex.getMessage() + " - Explanation: The length of data provided to a block cipher is incorrect, i.e., does not match the block size of the cipher");
            }
            catch (BadPaddingException ex){
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in encryption part. Reason: " + ex.getMessage() + " - Explanation: A particular padding mechanism is expected for the input data but the data is not padded properly (Most probably wrong/corrupt key caused this)");
            }
            catch (UnsupportedEncodingException ex){
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in encryption part. Reason: " + ex.getMessage() + " - Explanation: The Character Encoding is not supported");
            }
            catch (Exception ex) {
                ex.printStackTrace();
                throw new RuntimeException(decClassMethodNameForLogging + "An error occurred in decryption part. Reason: " + ex.getMessage());
            }
    
            return decCon;
        }
    
        public static void main(String args[]) {
            System.out.println("-- Original -------------");
            String plainText = "hello world";
            System.out.println("-- origWord : " + plainText + "\n");
    
            String e = Crypto.enc(plainText);
            String d = Crypto.dec(e);
    
            System.out.println("-- Results --------------");
            System.out.println("-- PlainText: " + plainText);
            System.out.println("-- EncryptedText: " + e);
            System.out.println("-- DecryptedText: " + d);
        }
    }
    

    Also, executable version is in below;

    https://www.jdoodle.com/a/19HT


    ORIGINAL ANSWER

    I see the written comments meet your needs but I want to share below solution for both your need as a code example and also for future reference;

    ** Using Randomized IV (Cipher Block Size is given for the IV size but static byte size can also be defined like for example '16 bytes')

    import javax.crypto.*;
    import javax.crypto.spec.SecretKeySpec;
    import java.util.Base64;
    import javax.xml.bind.DatatypeConverter;
    import java.security.SecureRandom;
    import javax.crypto.spec.IvParameterSpec;
    
    public class Crypto {
        public static String enc(String content, String key) {
            String encCon = "";
            String ivStr = "";
    
            try {
                SecretKeySpec keySpec = new SecretKeySpec(key.getBytes("UTF-8"), "Blowfish");
                Cipher cipher = Cipher.getInstance("Blowfish/CBC/PKCS5Padding");
    
                byte[] iv = new byte[cipher.getBlockSize()];
                SecureRandom secureRandom = new SecureRandom();
                secureRandom.nextBytes(iv);
                IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
    
                String secret = content;
                cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivParameterSpec);
                byte[] encoding = cipher.doFinal(secret.getBytes("UTF-8"));
    
                System.out.println("-- Encrypted -----------");
                encCon = DatatypeConverter.printBase64Binary(encoding);
                ivStr = DatatypeConverter.printBase64Binary(iv);
                System.out.println("-- encCon : " + encCon);
                System.out.println("-- iv : " + ivStr);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
    
            return encCon + ":" + ivStr;
        }
    
        public static String dec(String encContent, String key) {
            String decCon = "";
    
            try {
                SecretKeySpec keySpec = new SecretKeySpec(key.getBytes("UTF-8"), "Blowfish");
                Cipher cipher = Cipher.getInstance("Blowfish/CBC/PKCS5Padding");
    
                byte[] iv = DatatypeConverter.parseBase64Binary(encContent.substring(encContent.indexOf(":") + 1));
    
                String secret = encContent.substring(0, encContent.indexOf(":"));
                cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv));
                byte[] decoding = cipher.doFinal(Base64.getDecoder().decode(secret));
    
                System.out.println("-- Decrypted -----------");
                decCon = new String(decoding, "UTF-8");
                System.out.println("-- decCon : " + decCon);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
    
            return decCon;
        }
    
        public static void main(String args[]) {
            String e = Crypto.enc("hello world", "key123");
            String d = Crypto.dec(e, "key123");
        }
    }
    

    Note: Of course more secure solution can be achieved. Above solution is given only to give a little insight.