Core Java

Creating Password-Based Encryption Keys

This article discusses creating password-based encryption PBE keys.

First a reminder of earlier points – as a rule you should, when practical, use the PBE key as a master key that is used solely to unlock a working key. This has three major benefits:

  • You can have multiple passwords, e.g., an escrowed recovery key,
  • You can change your password without being forced to reencrypt everything,
  • You can change your working key without being forced to change your password.

I’ll discuss using working keys in database encryption in a future article.

Password-Based Encryption Key Generation with PBKDF2WithHmacSHA1

Java has not had a standard way to create a PBE Key in the past. Individual cryptographic providers provided their own generators but it was a painful process to identify and use a generator that matched your cipher.

This changed with Java 7. There is now a standard key generation algorithm that is available in all JCE implementations. It can definitely be used to produce AES keys. I’ve seen an example of it being used to produce a key of arbitrary length but I haven’t been able to duplicate that behavior – it may be a nonstandard extension.

This algorithm takes four parameters. The first is the key length – use 128 for AES keys. Other possible values are 192 and 256 bits. The second is the number of iterations. Your wifi router uses 4096 iterations but many people now recommend at least 10,000 iterations.

The third parameter is the ‘salt’. A wifi router uses the SSID, many sites use a small file, and I’ll discuss another approach below. The salt should be large enough that the entropy is greater than the key length. E.g., if you want a 128-bit key you should either have (at least) 128 bits of random binary data or about 22 random alpha-numeric characters.

The final parameter is the password. Again the entropy should be greater than the key length. In a webapp the password is often provided by the app server via JNDI.

Finally we want both a cipher key and an IV, not just a cipher key. The lack of an IV, or the use of a weak one, is one of the most common mistakes made by people unfamiliar with cryptography. (See: Not using a random initialization vector with cipher block chaining mode [owasp.org].) One common approach is to generate a random salt and prepend it to the ciphertext for use during decryption but I’ll discuss a different approach that uses the password and salt.

Now the code. First we see how to create a cipher key and IV from a password and salt. (We’ll discuss the salt in a moment.)

public class PbkTest {
    private static final Provider bc = new BouncyCastleProvider();
    private static final ResourceBundle BUNDLE = ResourceBundle
            .getBundle(PbkTest.class.getName());
    private SecretKey cipherKey;
    private AlgorithmParameterSpec ivSpec;

    /**
     * Create secret key and IV from password.
     * 
     * Implementation note: I've believe I've seen other code that can extract
     * the random bits for the IV directly from the PBEKeySpec but I haven't
     * been able to duplicate it. It might have been a BouncyCastle extension.
     * 
     * @throws Exception
     */
    public void createKeyAndIv(char[] password) throws SecurityException,
            NoSuchAlgorithmException, InvalidKeySpecException {
        final String algorithm = "PBKDF2WithHmacSHA1";
        final SecretKeyFactory factory = SecretKeyFactory
                .getInstance(algorithm);
        final int derivedKeyLength = 128;
        final int iterations = 10000;

        // create salt
        final byte[][] salt = feistelSha1Hash(createSalt(), 1000);

        // create cipher key
        final PBEKeySpec cipherSpec = new PBEKeySpec(password, salt[0],
                iterations, derivedKeyLength);
        cipherKey = factory.generateSecret(cipherSpec);
        cipherSpec.clearPassword();

        // create IV. This is just one of many approaches. You do
        // not want to use the same salt used in creating the PBEKey.
        try {
            final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding", bc);
            cipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(
                    salt[1], 0, 16));
            ivSpec = new IvParameterSpec(cipher.doFinal(salt[1], 4, 16));
        } catch (NoSuchPaddingException e) {
            throw new SecurityException("unable to create IV", e);
        } catch (InvalidAlgorithmParameterException e) {
            throw new SecurityException("unable to create IV", e);
        } catch (InvalidKeyException e) {
            throw new SecurityException("unable to create IV", e);
        } catch (BadPaddingException e) {
            throw new SecurityException("unable to create IV", e);
        } catch (IllegalBlockSizeException e) {
            throw new SecurityException("unable to create IV", e);
        }
    }
}

We could simply load a file containing random binary data but using a Feistel cipher allows us to mix entropy from two sources.

/**
     * Create salt. Two values are provided to support creation of both a cipher
     * key and IV from a single password.
     * 
     * The 'left' salt is pulled from a file outside of the app context. this
     * makes it much harder for a compromised app to obtain or modify this
     * value. You could read it as classloader resource but that's not really
     * different from the properties file used below. Another possibility is to
     * load it from a read-only value in a database, ideally one with a
     * different schema than the rest of the application. (It could even be an
     * in-memory database such as H2 that contains nothing but keying material,
     * again initialized from a file outside of the app context.)
     * 
     * The 'right' salt is pulled from a properties file. It is possible to use
     * a base64-encoded value but administration is a lot easier if we just take
     * an arbitrary string and hash it ourselves. At a minimum it should be a
     * random mix-cased string of at least (120/5 = 24) characters.
     * 
     * The generated salts are equally strong.
     * 
     * Implementation note: since this is for demonstration purposes a static
     * string in used in place of reading an external file.
     */
    public byte[][] createSalt() throws NoSuchAlgorithmException {
        final MessageDigest digest = MessageDigest.getInstance("SHA1");
        final byte[] left = new byte[20]; // fall back to all zeroes
        final byte[] right = new byte[20]; // fall back to all zeroes

        // load value from file or database.
        // note: we use fixed value for demonstration purposes.
        final String leftValue = "this string should be read from file or database";
        if (leftValue != null) {
            System.arraycopy(digest.digest(leftValue.getBytes()), 0, left, 0,
                    left.length);
            digest.reset();
        }

        // load value from resource bundle.
        final String rightValue = BUNDLE.getString("salt");
        if (rightValue != null) {
            System.arraycopy(digest.digest(rightValue.getBytes()), 0, right, 0,
                    right.length);
            digest.reset();
        }

        final byte[][] salt = feistelSha1Hash(new byte[][] { left, right },
                1000);

        return salt;
    }

A practical implementation using both a resource bundle (which is visible in the classpath) and a string loaded from the filesystem or database is:

/**
     * Create salt. Two values are provided to support creation of both a cipher
     * key and IV from a single password.
     * 
     * The 'left' salt is pulled from a file outside of the app context. this
     * makes it much harder for a compromised app to obtain or modify this
     * value. You could read it as classloader resource but that's not really
     * different from the properties file used below. Another possibility is to
     * load it from a read-only value in a database, ideally one with a
     * different schema than the rest of the application. (It could even be an
     * in-memory database such as H2 that contains nothing but keying material,
     * again initialized from a file outside of the app context.)
     * 
     * The 'right' salt is pulled from a properties file. It is possible to use
     * a base64-encoded value but administration is a lot easier if we just take
     * an arbitrary string and hash it ourselves. At a minimum it should be a
     * random mix-cased string of at least (120/5 = 24) characters.
     * 
     * The generated salts are equally strong.
     * 
     * Implementation note: since this is for demonstration purposes a static
     * string in used in place of reading an external file.
     */
    public byte[][] createSalt() throws NoSuchAlgorithmException {
        final MessageDigest digest = MessageDigest.getInstance("SHA1");
        final byte[] left = new byte[20]; // fall back to all zeroes
        final byte[] right = new byte[20]; // fall back to all zeroes

        // load value from file or database.
        // note: we use fixed value for demonstration purposes.
        final String leftValue = "this string should be read from file or database";
        if (leftValue != null) {
            System.arraycopy(digest.digest(leftValue.getBytes()), 0, left, 0,
                    left.length);
            digest.reset();
        }

        // load value from resource bundle.
        final String rightValue = BUNDLE.getString("salt");
        if (rightValue != null) {
            System.arraycopy(digest.digest(rightValue.getBytes()), 0, right, 0,
                    right.length);
            digest.reset();
        }

        final byte[][] salt = feistelSha1Hash(new byte[][] { left, right },
                1000);

        return salt;
    }

Finally we can see it in practice in two test methods:

/**
     * Obtain password. Architectually we'll want good "separation of concerns"
     * and we should get the cipher key and IV from a separate place than where
     * we use it.
     * 
     * This is a unit test so the password is stored in a properties file. In
     * practice we'll want to get it from JNDI from an appserver, or at least a
     * file outside of the appserver's directory.
     * 
     * @throws Exception
     */
    @Before
    public void setup() throws Exception {
        createKeyAndIv(BUNDLE.getString("password").toCharArray());
    }

    /**
     * Test encryption.
     * 
     * @throws Exception
     */
    @Test
    public void testEncryption() throws Exception {
        String plaintext = BUNDLE.getString("plaintext");

        Cipher cipher = Cipher.getInstance(BUNDLE.getString("algorithm"), bc);
        cipher.init(Cipher.ENCRYPT_MODE, cipherKey, ivSpec);
        byte[] actual = cipher.doFinal(plaintext.getBytes());
        assertEquals(BUNDLE.getString("ciphertext"),
                new String(Base64.encode(actual), Charset.forName("UTF-8")));
    }

    /**
     * Test decryption.
     * 
     * @throws Exception
     */
    @Test
    public void testEncryptionAndDecryption() throws Exception {
        String ciphertext = BUNDLE.getString("ciphertext");

        Cipher cipher = Cipher.getInstance(BUNDLE.getString("algorithm"), bc);
        cipher.init(Cipher.DECRYPT_MODE, cipherKey, ivSpec);
        byte[] actual = cipher.doFinal(Base64.decode(ciphertext));

        assertEquals(BUNDLE.getString("plaintext"),
                new String(actual, Charset.forName("UTF-8")));
    }

 

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button