Encrypted Properties

In Java, the java.util.Properties class allows access to persistent properties defined in a file. Those values are kept as key=value pairs. For sensitive data (such as passwords or access codes), these files provide direct access to the values by simply opening them. Therefore, it is sometimes necessary to encrypt those values. In this post, I will present an extension to Properties that utilizes AES encryption.

Encrypted Properties

EncryptedProperties allows one to store ciphertext as values in a properties file using AES encryption discussed in a previous post. It overrides getProperty and setProperty to facilitate encryption and decryption, and provides additional methods for reading and writing plaintext values – getPropertyPlain and setPropertyPlain. By default, it will employ AES-128, but the user can specify a desired supported value. For portability, the encrypted properties, initialization vector, and random salt files must be kept together.

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Base64;
import java.util.Properties;
 
/**
 * This class extends {@link Properties} for encryption and decryption of 
 * property values using AES. It automatically creates the IV and SALT files
 * for repetitive use, but you must include them with the properties file. These
 * files will be in the 
 * @author Ray Hylock
 */
public class EncryptedProperties extends Properties {
    private final static String CHARSET = "UTF-16";
    private static final int DEFAULT_KEY_LENGTH = 128;  // assumes no JCE
    private final AES aes;
    private final File file;
 
    /**
     * Instantiates a new {@link EncryptedProperties} object using 
     * AES-{@link #DEFAULT_KEY_LENGTH} encryption.
     * @param fileName      the file name
     * @param passPhrase    the encryption/decryption pass phrase
     * @throws Exception 
     */
    public EncryptedProperties(String fileName, char[] passPhrase) 
            throws Exception {
        this("", fileName, passPhrase);
    }
 
    /**
     * Instantiates a new {@link EncryptedProperties} object using 
     * AES-{@link #DEFAULT_KEY_LENGTH} encryption.
     * @param filePath      the file path to automatically load
     * @param fileName      the file name
     * @param passPhrase    the encryption/decryption pass phrase
     * @throws Exception 
     */
    public EncryptedProperties(String filePath, String fileName, 
            char[] passPhrase) throws Exception {
        this(filePath, fileName, passPhrase, DEFAULT_KEY_LENGTH);
    }
 
    /**
     * Instantiates a new {@link EncryptedProperties} object using 
     * AES-{@code keyLength} encryption.
     * @param fileName      the file name
     * @param passPhrase    the encryption/decryption pass phrase
     * @param keyLength     the key length
     * @throws Exception 
     */
    public EncryptedProperties(String fileName, char[] passPhrase, 
            int keyLength) throws Exception {
        this("", fileName, passPhrase, keyLength);
    }
 
    /**
     * Instantiates a new {@link EncryptedProperties} object using 
     * AES-{@code keyLength} encryption.
     * @param filePath      the file path to automatically load
     * @param fileName      the file name
     * @param passPhrase    the encryption/decryption pass phrase
     * @param keyLength     the key length
     * @throws Exception 
     */
    public EncryptedProperties(String filePath, String fileName, 
            char[] passPhrase, int keyLength) throws Exception {
        String fp = filePath.trim();
        if(fp.length() > 0){
            char c = fp.charAt(fp.length()-1);
            fp = (c == '/' || c == '\\') ? fp : fp + "/"; 
        }
        file = new File(fp+fileName);
        if(!file.exists()) {
            file.getParentFile().mkdirs();
            file.createNewFile();
        }
        super.load(new FileInputStream(file));
        aes = new AES(filePath, passPhrase, keyLength);
    }
     
    /**
     * Overrides the {@link Properties#getProperty(java.lang.String)} method. 
     * It either returns the {@code value} matching the property {@code key}, 
     * or throws a new {@code RuntimException}.
     * @param key   the property name
     * @return      the decrypted properties value
     */
    @Override
    public synchronized String getProperty(String key) {
        return getProperty(key, null);
    }
 
    /**
     * Overrides the {@link Properties#getProperty(java.lang.String, java.lang.String)} 
     * method. It either returns the {@code value} matching the property {@code key},
     * the {@code defaultValue}, or throws a new {@code RuntimException}.
     * @param key           the property name
     * @param defaultValue  the default value
     * @return              the decrypted properties value
     */
    @Override
    public synchronized String getProperty(String key, String defaultValue) {
        try {
            String v = super.getProperty(key);
            return (v == null && defaultValue != null) ? defaultValue : 
                    new String(aes.decrypt(Base64.getDecoder().decode(v)), CHARSET);
        } catch (Exception ex) {
            throw new RuntimeException("Could not decrypt the property '" + key + "'");
        }
    }
 
    /**
     * Gets a property stored as plaintext. It either returns the {@code value} 
     * matching the property {@code key} or throws a new {@code RuntimException}.
     * @param key   the property name
     * @return      the properties value
     */
    public synchronized String getPropertyPlain(String key) {
        return super.getProperty(key);
    }
 
    /**
     * Gets a property stored as plaintext. It either returns the {@code value} 
     * matching the property {@code key}, the {@code defaultValue}, or throws a 
     * new {@code RuntimException}.
     * @param key           the property name
     * @param defaultValue  the default value
     * @return              the properties value
     */
    public synchronized String getPropertyPlain(String key, String defaultValue) {
        // cannot call super.getProperty(key, defaultValue) because it calls
        // getProperty(key), which is overriden here and is for encrypted
        // properties, thus we have to do this manually
        String v = super.getProperty(key);
        return (v == null) ? defaultValue : v;
    }
 
    /**
     * Overrides {@link Properties#setProperty(java.lang.String, java.lang.String)}.
     * It encrypts the {@code value} and then sets the property (defined by 
     * {@code key}).
     * @param key       the property to set
     * @param value     the value to encrypt and set
     * @return          the previous value of the specified key in this hashtable,
     *                  or {@code null} if it did not have one   
     */
    @Override
    public synchronized Object setProperty(String key, String value) {
        return setProperty(key, value.toCharArray());
    }
 
    /**
     * Sets the property as plaintext.
     * @param key       the property to set
     * @param value     the value to encrypt and set
     * @return          the previous value of the specified key in this hashtable,
     *                  or {@code null} if it did not have one   
     */
    public synchronized Object setPropertyPlain(String key, String value) {
        try {
            Object o = super.setProperty(key, value);
            FileOutputStream out = new FileOutputStream(file);
            super.store(out, "Encrypted Properties File");
            out.flush();
            out.close();
            return o;
        } catch (Exception ex) {
            throw new RuntimeException("Could not set the value for property '"
                    + key + "'");
        }
    }
 
    /**
     * It encrypts the {@code value} and then sets the property (defined by 
     * {@code key}).
     * @param key       the property to set
     * @param value     the value to encrypt and set
     * @return          the previous value of the specified key in this hashtable,
     *                  or {@code null} if it did not have one   
     */
    public synchronized Object setProperty(String key, char[] value) {
        try {
            Object o = super.setProperty(key, 
                    Base64.getEncoder().encodeToString(aes.encrypt(toBytes(value))));
            FileOutputStream out = new FileOutputStream(file);
            super.store(out, "Encrypted Properties File");
            out.flush();
            out.close();
            return o;
        } catch (Exception ex) {
            throw new RuntimeException("Could not encrypt the value for property '"
                    + key + "'");
        }
    }
     
    /**
     * Returns the character array as a byte array based on {@link #CHARSET}.
     * @param chars the characters
     * @return      the byte array
     */
    private synchronized byte[] toBytes(char[] chars) {
        CharBuffer charBuffer = CharBuffer.wrap(chars);
        ByteBuffer byteBuffer = Charset.forName(CHARSET).encode(charBuffer);
        byte[] bytes = Arrays.copyOfRange(byteBuffer.array(),
                byteBuffer.position(), byteBuffer.limit());
        Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data
        Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
        return bytes;
    }
}
Test Class

Below is a simple test class. It creates a folder named “props” (line 7) that will ultimately contain the encrypted properties file “prop.enc” (line 8) and AES files “iv_256.k” and “salt_256.k”.

/**
 * This class tests <code>EncryptedProperties</code>.
 * @author Ray Hylock
 */
public class EncryptedPropertiesTest {
    // required variables
    private static final String FILE_PATH = "props";    // file path
    private static final String FILE_NAME = "prop.enc"; // file name
    private static final int KEY_LENGTH = 256;
    private char[] PASSWORD = {'I','j','d','u','y','e','k','j','*','&','5','k',
        'd','7','5',';','l','k','U','-','K','h','j','r','o','i','h','4'};
    private EncryptedProperties encprop;                // the properties file
     
    // variables used in test
    private final String key = "property_name";         // a test property name
    private final String value = "property_value";      // a test property value
     
    public static void main(String[] args) throws Exception {
        EncryptedPropertiesTest ept = new EncryptedPropertiesTest();
        ept.test();
    }
     
    private void test() throws Exception{
        // set up encryption and decryption properties file
        encprop = new EncryptedProperties(FILE_PATH, FILE_NAME, PASSWORD, KEY_LENGTH);
         
        // clear password
        java.util.Arrays.fill(PASSWORD, '\u0000');
        PASSWORD = null;
         
        // clear contents for this test
        encprop.clear();
         
        // set value
        System.out.println(String.format("Setting %s to encrypt(%s)", key, value));
        encprop.setProperty(key, value);
         
        // get value
        String result = encprop.getProperty(key);
        System.out.println(String.format("Getting %s = decrypt(encrytpt(%s)) "
                + "= %s", key, value, result));
    }
}

The output for the above example is:

Setting property_name to encrypt(property_value)
Getting property_name from decrypt(encrytpt(property_value)) = property_value