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