1

I've done some reading about implementing AES256 and deriving a key from a password. If I understand correctly:

  • I want to generate a new salt (for the key) and a new IV (for the encrypted message) for every new message.
  • It should also not be a problem sending the salt and the IV together with the message.

I decided to pack everything together in one byte array where the first 16 bytes is the salt, the next 16 bytes is the IV and the rest is the encrypted message. This with a key length of 256 bits and 20000 iterations for generating the key. I also encode the whole thing in Base64 for transmission.

Can this approach be improved? Knowing that I'm limited to 256 bytes for the complete message (salt+iv+message). How secure is this?

(Feedback and changes at the bottom)

public class app {

    public static void main(String[] args) throws Exception {
        int iterations = 20000;
        int keyLength = 32;
        byte[] salt = getRandomBytes(16);
        byte[] iv = getRandomBytes(16);
        char[] password = "password_here".toCharArray();
        byte[] payload = "payload_here".getBytes();

        byte[] key = deriveKey(iterations, keyLength, salt, iv, password);
        byte[] encrypted = encrypt(iv, key, payload);
        byte[] bytes = concatBytes(salt, iv, encrypted);

        String output = Base64.encodeBytes(bytes);
        int outputLength = output.getBytes().length;
        System.out.println(outputLength + "\n" + output);
    }

    private static byte[] deriveKey(int iterations, int length, byte[] salt, byte[] iv, char[] password)
            throws InvalidKeySpecException, NoSuchAlgorithmException {
        PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, length * 8);
        SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        byte[] key = skf.generateSecret(spec).getEncoded();
        return key;
    }

    private static byte[] getRandomBytes(int length) throws NoSuchAlgorithmException {
        SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
        byte[] salt = new byte[length];
        sr.nextBytes(salt);
        return salt;
    }

    private static byte[] concatBytes(byte[]... arrays) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        for (byte[] array : arrays)
            outputStream.write(array);
        return outputStream.toByteArray();
    }

    private static byte[] encrypt(byte[] iv, byte[] key, byte[] text) throws NoSuchAlgorithmException,
            NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException,
            BadPaddingException {
        AlgorithmParameterSpec ivSpec = new IvParameterSpec(iv);
        SecretKeySpec newKey = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, newKey, ivSpec);
        return cipher.doFinal(text);
    }
}

Based on feedback I've made the following changes:

  • Implemented authenticated encryption by using GCM. I had to switch to the Bouncy Castle library to be able to use GCM.
  • I dropped the IV and am using the same bytes for the key salt and the GCM nonce.
  • Upped the number of iterations for deriving the key to 50k.

Resulting code:

public class app {

    public static void main(String[] args) throws Exception {

        Security.addProvider(new BouncyCastleProvider());

        int iterations = 50000;
        int keyLength = 32;
        byte[] salt = getRandomBytes(16);
        char[] password = "password_here".toCharArray();
        byte[] payload = "payload_here".getBytes();

        byte[] key = deriveKey(iterations, keyLength, salt, password);
        byte[] encrypted = encrypt(salt, key, payload);
        byte[] bytes = concatBytes(salt, encrypted);

        String output = Base64.encodeBytes(bytes);
        int outputLength = output.getBytes().length;
        System.out.println(outputLength + "\n" + output);
    }

    private static byte[] deriveKey(int iterations, int length, byte[] salt, char[] password)
            throws InvalidKeySpecException, NoSuchAlgorithmException {
        PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, length * 8);
        SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        return skf.generateSecret(spec).getEncoded();
    }

    private static byte[] getRandomBytes(int length) throws NoSuchAlgorithmException {
        SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
        byte[] bytes = new byte[length];
        sr.nextBytes(bytes);
        return bytes;
    }

    private static byte[] concatBytes(byte[]... arrays) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        for (byte[] array : arrays)
            outputStream.write(array);
        return outputStream.toByteArray();
    }

    private static byte[] encrypt(byte[] nonce, byte[] key, byte[] text) throws NoSuchAlgorithmException,
            NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException,
            BadPaddingException, IllegalStateException, InvalidCipherTextException {

        GCMBlockCipher gcm = new GCMBlockCipher(new AESFastEngine());
        AEADParameters params = new AEADParameters(new KeyParameter(key), 128, nonce);
        gcm.init(true, params);

        byte[] output = new byte[gcm.getOutputSize(text.length)];
        int len = gcm.processBytes(text, 0, text.length, output, 0);
        gcm.doFinal(output, len);

        return output;
    }
}
Mike Edward Moras
  • 18,161
  • 12
  • 87
  • 240
ndsc
  • 111
  • 4

1 Answers1

2

There are other ways to "win" space. $\:$ Also, see this answer regarding compression.

You can remove (or just reduce the size of) the IV, since if the salts
are different then the derived keys should be sufficiently independent.

You can make the salt smaller than what it should be if bandwidth wasn't an issue.
Additionally, the salt length can depend on the message's length, although
it must be simple to figure out which part of an overall ciphertext is the salt.


Instead of using a standard authenticated encryption system, you can build a variable-width block cipher by having the inner PRF keys depend pseudorandomly on the block size, and handle authentication by

$\;$ choosing how the non-negative integer value(s) of $\sigma$ depend on the message's length,
$\;$ although it must be simple to calculate $\sigma$ given $\;\; \sigma + $ [the message's length]
$\;\;\;$ and
$\;$ choosing either "beginning" or "end"
$\;\;\;$ and
$\;$ having the encryptor put that many zeros at that part of the message
$\;$ before applying the forward direction of the variable-width block cipher
$\;\;\;$ and
$\;$ having the decryptor reject if the result of applying the reverse direction of the variable-width
$\;$ block cipher to the relevant part of the ciphertext does not have $\sigma$ zeros at its chosen end

.


The probability of a forgery by an efficient adversary
will be at most negligibly more than $\:\:$(# of tries)/(2^$\sigma$)$\;\;$.

The real decryption results will be computationally indistinguishable from the results that are:
reject whenever the real decryptor would, and otherwise output a message chosen
randomly from among those whose length's are compatible with the ciphertext's
length but were not themselves encrypted with the password and the ciphertext's salt.
(If you do use an IV, then the last word in the previous sentence should be replaced with "salt and IV".)