· Alex · security  · 10 min read

A Reasonably Secure Way To Store a Secret with Java

How to use AES GCM and PBKDF2 to store an encrypted secret

How to use AES GCM and PBKDF2 to store an encrypted secret

A Reasonably Secure Way To Store a Secret with Java

Update: this was written in 2019 and some things might have changed since.

Suppose you need to securely store a secret, like a password for a .p12 certificate. Or perhaps you handle some personal data which must be encrypted with the user’s password. It needs to be reasonably secure, following security best practices and should use proper encryption algorithms for 2019.

At the same time, I don’t want to add any new dependencies to my Java project so we’re going to use only what’s available in Java by default (see JCA).

Now, let’s consider this scenario: the user supplies a password and a secret. He needs to store & encrypt said secret, such that nobody else can access it without knowing the password.

Sounds simple, right? We would use the entered password to encrypt some secret using good, old AES. The thing is, AES has a constraint of using a certain key length (128, 192 or 256 bits) for encryption. So the user’s password needs to be that length or modified in such way to match that key size requirement. We cannot force the length requirement for the user, but we can use a process called key derivation to make the key fit AES’ key constraints.

First rule of crypto is do not roll your own crypto. So we’re not going to. We’re going to use known algorithms and methods. There are quite a few great resources online with recommendations for key derivation and encryption (google NIST). Of course, choosing a proper algorithm isn’t enough most of the time. The parameters you pass to the algorithm are just as important and play a crucial role in how secure the result will be.

What is key derivation?

Key derivation is the process of stretching a key or converting it into a certain format using a pseudorandom Key Derivation Function (KDF). We can apply the KDF to the users’s password and use the result as a derived password to do the symmetric encryption with AES.

Key derivation implementation

For key derivation, we’re going to use PBKDF2, according to NIST 800-132. An implementation for it is found in Java. And it’s really easy to use!

There are alternatives to PBKDF2, such as bcrypt and scrypt (pronounced “ess scrypt”), which are newer and perhaps safer, but unfortunately there isn’t an implementation for either of them in Java at the time of writing (2019). You can find open-source packages to use, if you don’t mind adding 3rd party dependencies to your project.

Be aware that NIST recommends using PBKDF2 as an algorithm for generating other cryptographic keys, not as a method to store something securely.

This is another reason why we use the derived key for AES encryption instead of directly using the password.

Why use a KDF?

The NIST document linked above has some answers. One of PBKDF2’s properties is that it makes brute-force and dictionary attacks much slower. Part of the reason behind this is that they were intentionally created slow to compute on GPUs. Another part of it is a configurable parameter which defines the number of rounds or iterations the derivation function will be applied. Another reason is that it’s already available in Java. No extra dependencies.

Encrypting with AES

AES is a secure encryption algorithm. It takes an IV (Initialization Vector), some plain text and a key and it outputs cipher text. There are different operation modes for AES. The older AES-CBC (Cipher Block Chaining) and AES-ECB (Electronic Code-Book) can be vulnerable to certain attack types such as padding oracles attacks, plaintext attacks etc.

We’ll be using a newer one (though not that new since it’s from 2008) operation mode called Galois/Counter Mode (GCM). Also called AES-GCM, this algorithm uses AES-CTR (Counter mode) for encryption, does not require padding (because CTR mode turns it from a block cipher into a stream cipher) and brings data authentication to the cipher text (using Galois Hash). Another advantage of GCM over the other modes (like CBC) is that it’s faster. This is because it can be computed in parallel. CBC can’t be done this way because in its encryption process, each block is XOR’ed with the previous (so it needs to be done sequentially). You can read more about AES-GCM here.

AES-GCM Authentication

First of all, what is authentication? In this case, it’s the process of proving cipher text to be genuine and not-tampered.

The cipher text is authenticated and optionally, random signed data (called an authentication tag) is added to the output. Security of AES-GCM heavily depends on this tag, so make sure to set it to 128 bits (in Java, using the GCMParameterSpec class).

With the introduction being over, let’s get coding our secure secret storage app.

Secure password storage in Java code

First, we need a few objects which will help us with cryptographic operations. We’ll create an init() method for that:

private void init() throws GeneralSecurityException {
    if (!initialized) {
        cipher = Cipher.getInstance(ALG_AES_GCM);
        factory = SecretKeyFactory.getInstance(ALG_PBKDF2_KEY_DERIVATION);
        initialized = true;
    }
}

The cipher object is used for AES-GCM, while factory is used to create the derived key from the user’s password. Both take a single String argument which contains the algorithm to be used.

Now we need a way to derive the user’s password and creating the derived key to be used in AES encryption:

private SecretKey getDerivedKey(final char[] pwd, final byte[] salt) throws GeneralSecurityException {
    final SecretKey key = factory.generateSecret(new PBEKeySpec(pwd, salt, ALG_PBKDF2_ITERATIONS, ALG_PBKDF2_LEN));
    return new SecretKeySpec(key.getEncoded(), "AES");
}

The method takes the following arguments:

  • char[] pwd -  the password as a char array
  • byte[] salt - a random salt as a byte array

And it returns a SecretKey object (which will contain the derived key).

Notice the ALG_PBKDF2_ITERATIONS and ALG_PBKDF2_LEN parameters of PBEKeySpec which have an important security impact.

The first one is the number of iterations for the PBKDF2 algorithm. The NIST document recommends 10.000, but that was a long time ago and computers progressed a lot, so I would go for something above 300.000, even as high as 1 million iterations if your application and hardware can afford it. You can experiment with different values according to your needs, but don’t go under 100.000.

The second parameter specifies the output length of the derivation process, hence we can use this to obtain the proper key length to be used in AES encryption.

Now, we need to create one more utility method:

private byte[] getSecureRand(final int length) {
    final byte[] rnd = new byte[length];
    new SecureRandom().nextBytes(rnd);
    return rnd;
}

It’s a very simple method that just gets length number of random bytes from Java’s SecureRandom PRNG (Pseudo Random Number Generator). This will be used to generate secure pseudorandom values to be used for the initialization vector and salt.

Prior to implementing the encrypt() and the decrypt() methods, we must create an object which will hold the output data. This can be used later for serialization and cleanup purposes (as we should do what we can to not keep password in memory, especially not in Strings, as they are immutable).

On passwords and the String Pool:

“In object-oriented and functional programming, an immutable object is an object whose state cannot be modified after it is created. This is in contrast to a mutable object, which can be modified after it is created”.

Every time we create a String object in Java it gets allocated somewhere in the heap, in a place called the String Pool. Some time after the object is no longer referenced, it will be eventually cleared from memory by the Garbage Collector (GC). The problem is we cannot control when that happens, since we cannot control the GC. This is why we should try to use byte/char arrays to store sensitive data as much as possible, since we can actually clear array content.

Here’s a class example:

public class CryptoData {
private byte[] salt = null;
private byte[] iv = null;
private byte[] data = null;
//...
}

Just generate setters and getters for those fields and that’s it.  Most modern IDEs can do this for you in a couple of clicks or a keyboard shortcut. Fields salt and iv will hold the random salt and initialization vector, while data will hold the cipher text (aka encrypted text) as a result of calling encrypt(). This object will also need to be passed to the decrypt() method in order to obtain the plaintext (in byte array form).

Encrypt method

Now let’s code the encrypt() method. It will take 2 arguments: a char array for the password and a byte array for the plain text to be encrypted:

public class CryptoData {
public CryptoData encrypt(char[] password, final byte[] input) throws GeneralSecurityException

Let’s generate the random salt and IV using the utility method we defined earlier:

final byte[] randomSalt = getSecureRand(256);
final byte[] iv = getSecureRand(ALG_AES_GCM_IV_LENGTH);

Now, initiate AES-GCM using the password, salt and IV:

cipher.init(Cipher.ENCRYPT_MODE, getDerivedKey(password, randomSalt), new GCMParameterSpec(ALG_AES_GCM_TAG_LENGTH, iv));

Finally, let’s perform the encryption and store the encrypted blob (cipher text), IV and salt:

CryptoData r = new CryptoData();
r.setData(cipher.doFinal(input));
r.setIv(iv);
r.setSalt(randomSalt);
return r;

Decrypt method

Now for the decrypt() function. We just do the same thing, but in reverse. The method is defined as:

public byte[] decrypt(char[] password, CryptoData cryptoData) throws GeneralSecurityException {

Its only arguments are the password and the cryptoData object obtained from encrypt(). You could also persist this object to disk and later load it from there.

First, we need to generate a derived key from the password, salt and IV. Then use it to decrypt the “data” field from CryptoData:

cipher.init(Cipher.DECRYPT_MODE, getDerivedKey(password, cryptoData.getSalt()), new GCMParameterSpec(ALG_AES_GCM_TAG_LENGTH, cryptoData.getIv()));
return cipher.doFinal(cryptoData.getData());

That’s it! You can recreate this in your application. It’s clearly not thread safe, so careful how you adapt it to your needs.

Complete Code Example

public static void main(String[] args) throws Exception {
    PasswordManager e = new PasswordManager();
 
    char[] userPass = "UberSecurePassword".toCharArray();
    byte[] thingToProtect = "MyPin".getBytes();
 
    CryptoData encrypted = e.encrypt(userPass, thingToProtect);
    System.out.println("encrypted: " + Arrays.toString(encrypted.getData()));
    System.out.println("plain text: " + new String(e.decrypt(userPass, encrypted), StandardCharsets.UTF_8));
    encrypted.clear();
}

Output:

encrypted: [24, -48, -19, 99, -4, -102, 59, -114, 1, 99, 84, -98, -92, -98, 21, 2, 89, -91, 11, -81, 76, 19, 39, 88, 98]

plain text: MyPin

Process finished with exit code 0

You’ll notice that every time you run it, you’ll get a different output for encrypted text. This is because of the random IV and salt. This is also why we need to pass them as parameters when decrypting.

Cleanup

Perhaps you noticed the encrypted.clear() method. This is something I added to the object that holds the data, which just zeros out the arrays:

if (salt!=null) Arrays.fill(salt, (byte) 0);
if (iv!=null) Arrays.fill(iv, (byte) 0);
if (data !=null) Arrays.fill(data, (byte) 0);

This method should be called once done with encrypting/decrypting. As mentioned earlier, this is an effort to not keep passwords in memory, although this is difficult to control in the Java world. Most times you will eventually use the password in a String, so it will end up in the pool. And who knows when it will eventually get dealt with by the garbage collector.

Now, to persist the result you can use your favorite json, xml, etc framework.

Base64 encoding for storing in database

Use Base64 encoding and store the results in a database. Base64 class is available from Java 8:

public byte[] getBase64Encoded (byte[] value) {
    return Base64.getEncoder().encode(value);
}
[...]
System.out.println("encrypted: " + new String(CryptoData.getBase64Encoded(encrypted.getData())));

Output:

encrypted: nv7UViJIOl54OOFfGGqjSQYLAyJxysaa2Q==

Storing as binary data

Or you could even store them in binary form:

encrypted.saveBinary("/tmp/binaryKey");
 
CryptoData cryptoDataReadFromDisk = CryptoData.loadBinary("/tmp/binaryKey");
System.out.println("plain text from disk: " + new String(e.decrypt(userPass, cryptoDataReadFromDisk), StandardCharsets.UTF_8));

Output:

plain text from disk: MyPin

Process finished with exit code 0

Here’s how the saveBinary() method could be implemented:

public void saveBinary(String path) throws IOException {
    DataOutputStream dout a = new DataOutputStream(new FileOutputStream(path));
    dout.write(sig);
    dout.writeInt(salt.length);
    dout.write(salt);
    dout.writeInt(iv.length);
    dout.write(iv);
    dout.writeInt(data.length);
    dout.write(data);
    dout.close();
}

First, we wrote a signature of 2 bytes (sig). You can write any constant you want. This is just a very basic method to avoid reading invalid files. Then, just write to the file: first the size of the array and the data for each field (salt, iv and data). 

The loadBinary() method is basically the same thing, but in reverse. You should also add an HMAC and size checks when reading the data back.

Conclusion

That’s it for this article on how to securely store a secret in Java. I hope this helped you, but if you have questions feel free to reach out. Thanks for reading!

About the Author:

Alex

Application Security Engineer and Red-Teamer. Over 15 years of experience in Application Security, Software Engineering and Offensive Security. OSCE3 & OSCP Certified. CTF nerd.

Back to Blog

Related Posts

View All Posts »
My OSCP Journey

My OSCP Journey

Common questions, my experience, preparation and methodology as well as tips to help you land the OSCP exam