9

I have an application that needs to store some secret passwords in a configuration file such as database and ftp passwords/detail. I've looked around and found a lot of encryption/decryption solutions using AES, but I can't seem to figure out how to make it work without changing the key. That means I can encrypt and decrypt (using the same SecretKey), but to maintain persistence across restarts etc. I can't seem to make the SecretKey stay the same. The example below shows my methods working:

String secret = Encryptor.encrpytString("This is secret");
String test = Encryptor.decrpytString(secret);
System.out.println(test); //This is secret is printed

So far so good. However if I run it once I might get the value of '2Vhht/L80UlQ184S3rlAWw==' as my secret, the next time it is 'MeC4zCf9S5wUUKAu8rvpCQ==', so presumably the key is changing. I'm assuming I am applying some counter-intuative logic to the problem and would appreciate if someone can shed some light on either a) what I'm doing wrong, or b) a solution that would allow me to store the password information encrypted and retrievable with the information provided.

My methods are as follows:

private static final String salt = "SaltySalt";

private static byte [] ivBytes = null;

private static byte[] getSaltBytes() throws Exception {
    return salt.getBytes("UTF-8");
}

private static char[] getMasterPassword() {
    return "SuperSecretPassword".toCharArray();
}

private static byte[] getIvBytes() throws Exception {
    if (ivBytes == null) {
        //I don't have the parameters, so I'll generate a dummy encryption to create them
        encrpytString("test");
    }
    return ivBytes;
}

public static String encrpytString (String input) throws Exception {
    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    PBEKeySpec spec = new PBEKeySpec(getMasterPassword(), getSaltBytes(), 65536,256);
    SecretKey secretKey = factory.generateSecret(spec);
    SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES");
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, secret);
    ivBytes = cipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV();
    byte[] encryptedTextBytes = cipher.doFinal(input.getBytes("UTF-8"));
    return DatatypeConverter.printBase64Binary(encryptedTextBytes);        
}

public static String decrpytString (String input) throws Exception {
    byte[] encryptedTextBytes = DatatypeConverter.parseBase64Binary(input);
    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    PBEKeySpec spec = new PBEKeySpec(getMasterPassword(), getSaltBytes(), 65536, 256);
    SecretKey secretKey = factory.generateSecret(spec);
    SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES");
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(getIvBytes()));
    byte[] decryptedTextBytes = cipher.doFinal(encryptedTextBytes);
    return new String(decryptedTextBytes);
}

Thanks for the help!

Aaron
  • 343
  • 3
  • 10

3 Answers3

6

OK, looks like I've found the answer to my question. I sourced my information from this Stackoverflow post. From what I understand, the IV (initialisation vector) is used to add entropy into the encryption process. Each time you create a new cipher, Java creates a slightly different IV. There are therefore two solutions:

  1. User a fixed IV, or
  2. Store the IV along with the encrypted data.

From what I've read, option 1 is not very good practice; so option 2 it is. I understand that it should be possible to simply append the IV to the encrypted string (as the secret is still required) and therefore the IV can be reconstructed when it comes time to decrypt.

Here is the almost complete solution. I'm still getting some padding errors on decryption (see my comment). I don't have time to spend on it now, so as a temporary measure I immediately try decrypting an encrypted string and keep on trying (iterating) until it works. It seems to have about a 50% hit rate + I'm not encrypting often enough for it to be a performance concern. Would be nice if someone could suggest a fix though (just for completeness sake).

private static final String salt = "SaltySalt";

private static final int IV_LENGTH = 16;

private static byte[] getSaltBytes() throws Exception {
    return salt.getBytes("UTF-8");
}

private static char[] getMasterPassword() {
    return "SuperSecretPassword".toCharArray();
}

public static String encrpytString (String input) throws Exception {
    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    PBEKeySpec spec = new PBEKeySpec(getMasterPassword(), getSaltBytes(), 65536,256);
    SecretKey secretKey = factory.generateSecret(spec);
    SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES");
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, secret);
    byte[] ivBytes = cipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV();
    byte[] encryptedTextBytes = cipher.doFinal(input.getBytes("UTF-8"));
    byte[] finalByteArray = new byte[ivBytes.length + encryptedTextBytes.length]; 
    System.arraycopy(ivBytes, 0, finalByteArray, 0, ivBytes.length);
    System.arraycopy(encryptedTextBytes, 0, finalByteArray, ivBytes.length, encryptedTextBytes.length);
    return DatatypeConverter.printBase64Binary(finalByteArray);        
}

public static String decrpytString (String input) throws Exception {
    if (input.length() <= IV_LENGTH) {
        throw new Exception("The input string is not long enough to contain the initialisation bytes and data.");
    }
    byte[] byteArray = DatatypeConverter.parseBase64Binary(input);
    byte[] ivBytes = new byte[IV_LENGTH];
    System.arraycopy(byteArray, 0, ivBytes, 0, 16);
    byte[] encryptedTextBytes = new byte[byteArray.length - ivBytes.length];
    System.arraycopy(byteArray, IV_LENGTH, encryptedTextBytes, 0, encryptedTextBytes.length);
    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    PBEKeySpec spec = new PBEKeySpec(getMasterPassword(), getSaltBytes(), 65536, 256);
    SecretKey secretKey = factory.generateSecret(spec);
    SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES");
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(ivBytes));
    byte[] decryptedTextBytes = cipher.doFinal(encryptedTextBytes);
    return new String(decryptedTextBytes);
}
Community
  • 1
  • 1
Aaron
  • 343
  • 3
  • 10
  • 1
    This is excatly what I have in a 3rd part integration interface. XML input data field Base64toBytes, bytes 0..15 are IV value, decrypt bytes 16..n with a secret key. – Whome Jul 06 '15 at 12:10
  • 2
    For AES/CBC/PKCS7Padding, most people choose prepend the IV to the cipher text. So on decryption, cut the first 16 bytes from the ciphertext, they are the IV, init the cipher and then doFinal with the rest. This has a few nice properties, most notably that implementations that don't follow this convention, but just start with a 0 IV and discard the first block still can decrypt the text. – wallenborn Jul 06 '15 at 12:12
  • Thanks for the confirmation guys! – Aaron Jul 07 '15 at 00:00
  • The plot thickens... still not 100% getting some "Given final block not properly padded" errors... – Aaron Jul 07 '15 at 05:36
  • Your approach is not secured enough. I strongly suggest you read this: https://stackoverflow.com/a/53015144/1235935 – Saptarshi Basu Nov 16 '18 at 03:11
1

Use a static Initialization Vector, e.g. a zero IV:

cipher.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(new byte[16]));

cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(new byte[16]));

Since you're storing passwords you probably want to use a random IV and/or random salt and store them with the cipher text so the same passwords don't encrypt to the same ciphertext.

ReyCharles
  • 1,772
  • 10
  • 30
  • 2
    Agreed using a static iv will do the trick, but also agree that's not good practice unless externally salting the text before storing it. Storing the IV along with the encrypted message seems to be the go. – Aaron Jul 08 '15 at 23:28
1

You need to setSeed() before

class Encryptor {

    static final String salt = "SaltSalt";

    public static byte[] encryptString(String input) throws Exception {
        byte[] bytes = input.getBytes("UTF-8");

        Cipher cipher = Cipher.getInstance("AES");
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        SecureRandom secureRandom = new SecureRandom();
        secureRandom.setSeed(salt.getBytes("UTF-8"));
        keyGenerator.init(256, secureRandom);
        Key key = keyGenerator.generateKey();
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] a = cipher.doFinal(bytes);
        return a;
    }

    public static String decryptString(byte[] input) throws Exception {
        Cipher cipher = Cipher.getInstance("AES");
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        SecureRandom secureRandom = new SecureRandom();
        secureRandom.setSeed(salt.getBytes("UTF-8"));
        keyGenerator.init(256, secureRandom);
        Key key = keyGenerator.generateKey();
        cipher.init(Cipher.DECRYPT_MODE, key);
        byte[] decrypted = cipher.doFinal(input);
        String result = new String(decrypted, "UTF-8");
        return result;
    }
}
AS Mackay
  • 2,831
  • 9
  • 19
  • 25
wiku0
  • 11
  • 1
  • 2
    I was about to downvote this. It is extremely vulnurable. You are using the default ECB mode. I strongly suggest you red this: https://stackoverflow.com/a/53015144/1235935 – Saptarshi Basu Nov 16 '18 at 03:32
  • 1
    It also doesn't work on any Java instance with a decent default for SecureRandom, which seems to be anything other than j<=8 on Windows. – dave_thompson_085 Nov 16 '18 at 11:27