Disclaimer: I’m not a Java internals specialist. Let me know if I got a point wrong here.
Wiping sensitive information when you don’t need it anymore is a very basic security rule. It makes sense to everyone when we talk about hard discs, but the same rule should be applied to memory (RAM). While memory management is quite straight forward in C derived languages (eg. allocate some memory, override it with random bytes when the sensitive information is not needed anymore), it’s quite hard to do it in Java. Is it even possible? Java Strings are immutable, that means you can not change the memory allocated for a String. The Java VM will make a copy of the String and change the copy. So from this perspective, Strings are not a good way to store sensitive information. So let’s do it in char arrays I thought and wrote the following Java class:
/** * ---------------------------------------------------------------------------- * "THE BEER-WARE LICENSE" (Revision 42): * <floyd at floyd dot ch> wrote this file. As long as you retain this notice you * can do whatever you want with this stuff. If we meet some day, and you think * this stuff is worth it, you can buy me a beer in return * floyd http://floyd.ch @floyd_ch <floyd at floyd dot ch> * August 2012 * ---------------------------------------------------------------------------- **/ import java.util.Arrays; import java.io.IOException; import java.io.Serializable; import java.lang.Exception; /** * ATTENTION: This method is only ASCII safe, not Unicode. * It should be used to store sensitive information which are ASCII * * This class keeps track of the passed in char[] and wipes all the old arrays * whenever they are changed. This means: * * char[] r1 = {0x41, 0x41, 0x41, 0x41, 0x41, 0x41}; * SecureString k = new SecureString(r1); * k.destroy(); * System.out.println(r1); //Attention! r1 will be {0x00, 0x00, 0x00, 0x00, 0x00, 0x00} * * char[] r2 = {0x41, 0x41, 0x41, 0x41, 0x41, 0x41}; * SecureString k = new SecureString(r2); * k.append('C'); * System.out.println(r2); //Attention! r2 will be {0x00, 0x00, 0x00, 0x00, 0x00, 0x00} * System.out.println(k.getByteArray()); //correct * * General rule for this class: * - Never call toString() of this class. It will just return null: * char[] r3 = {0x41, 0x41, 0x41, 0x41, 0x41, 0x41}; * SecureString k = new SecureString(r3); * System.out.println(k); //will print null, because it calls toString() * * - Choose the place to do the destroy() wisely. It should be done when an operation * is finished and the string is not needed anymore. */ public class SecureString implements Serializable{ private static final long serialVersionUID = 1L; private char[] charRepresentation; private char[] charRepresentationLower; private byte[] byteRepresentation; private boolean isDestroyed = false; private int length=0; private final static int OBFUSCATOR_BYTE = 0x55; /** * After calling the constructor you should NOT wipe the char[] * you just passed in (the stringAsChars reference) * @param stringAsChars */ public SecureString(char[] stringAsChars){ //Pointing to the same address as original char[] this.length = stringAsChars.length; this.initialise(stringAsChars); } /** * After calling the constructor you should NOT wipe the byte[] * you just passed in (the stringAsBytes reference) * @param stringAsBytes */ public SecureString(byte[] stringAsBytes){ //Pointing to the same address as original byte[] this.length = stringAsBytes.length; this.initialise(stringAsBytes); } /** * @return length of the string */ public int length(){ return this.length; } /** * Initialising entire object based on a char array * @param stringAsChars */ private void initialise(char[] stringAsChars){ this.length = stringAsChars.length; charRepresentation = stringAsChars; charRepresentationLower = new char[stringAsChars.length]; byteRepresentation = new byte[stringAsChars.length]; for(int i=0; i<stringAsChars.length; i++){ charRepresentationLower[i] = Character.toLowerCase(charRepresentation[i]); byteRepresentation[i] = (byte)charRepresentation[i]; } } /** * Initializing entire object based on a byte array * @param stringAsBytes */ private void initialise(byte[] stringAsBytes){ this.length = stringAsBytes.length; byteRepresentation = stringAsBytes; charRepresentation = new char[stringAsBytes.length]; charRepresentationLower = new char[stringAsBytes.length]; for(int i=0; i<stringAsBytes.length; i++){ charRepresentation[i] = (char) byteRepresentation[i]; charRepresentationLower[i] = Character.toLowerCase(charRepresentation[i]); } } /** * We first obfuscate the string by XORing with OBFUSCATOR_BYTE * So it doesn't get serialized as plain text. THIS METHOD * WILL DESTROY THIS OBJECT AT THE END. * @param out * @throws IOException */ private void writeObject(java.io.ObjectOutputStream out) throws IOException{ out.writeInt(byteRepresentation.length); for(int i = 0; i < byteRepresentation.length; i++ ){ out.write((byte)(byteRepresentation[i]) ^ (byte)(OBFUSCATOR_BYTE)); } //TODO: Test if the Android Intent is really only calling writeObject once, //otherwise this could be a problem. Then I suggest that the SecureString //is not passed in an intent (and never serialized) destroy(); } /** * We first have to deobfuscate the string by XORing with OBFUSCATOR_BYTE * @param out * @throws IOException */ private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ //we first have to deobfuscate int newLength = in.readInt(); byteRepresentation = new byte[newLength]; for(int i = 0; i < newLength; i++ ){ byteRepresentation[i] = (byte) ((byte)(in.read()) ^ (byte)(OBFUSCATOR_BYTE)); } initialise(byteRepresentation); } /** * Use this method wisely * * When the destroy() method of this SecureString is called, the byte[] * this method had returned will also be wiped! * * Never WRITE directly to this byte[], but only READ * * @return reference to byte[] of this SecureString */ public byte[] getByteArray(){ if(this.isDestroyed) return null; return byteRepresentation; } /** * This method should never be called. */ @Deprecated @Override public String toString(){ return null; } /** * This method should never be called, because it means you already stored the * sensitive information in a String. * @param string * @return * @throws Exception */ @Deprecated public boolean equals(String string) throws Exception{ throw new Exception("YOU ARE NOT SUPPOSED TO CALL equals(String string) of SecureString, "+ "because your are not supposed to have the other value in a string in the first place!"); } /** * This method should never be called, because it means you already stored the * sensitive information in a String. * @param string * @return * @throws Exception */ @Deprecated public boolean equalsIgnoreCase(String string) throws Exception{ throw new Exception("YOU ARE NOT SUPPOSED TO CALL equalsIgnoreCase(String string) of SecureString, "+ "because your are not supposed to have the other value in a string in the first place!"); } /** * Comparing if the two strings stored in the SecureString objects are equal * @param secureString2 * @return true if strings are equal */ public boolean equals(SecureString secureString2){ if(this.isDestroyed) return false; return Arrays.equals(this.charRepresentation, secureString2.charRepresentation); } /** * Comparing if the two strings stored in the SecureString objects are equalsIgnoreCase * @param secureString2 * @return true if strings are equal ignoring case */ public boolean equalsIgnoreCase(SecureString secureString2){ if(this.isDestroyed) return false; return Arrays.equals(this.charRepresentationLower, secureString2.charRepresentationLower); } /** * Delete a char at the given position * @param index */ public void deleteCharAt(int index){ if(this.isDestroyed) return; if(this.length == 0 || index >= length) return; wipe(charRepresentationLower); wipe(byteRepresentation); char[] newCharRepresentation = new char[length-1]; for(int i=0; i<index; i++) newCharRepresentation[i] = charRepresentation[i]; for(int i=index+1; i<this.length-1; i++) newCharRepresentation[i-1] = charRepresentation[i]; wipe(charRepresentation); initialise(newCharRepresentation); } /** * Append a char at the end of the string * @param c */ public void append(char c){ wipe(charRepresentationLower); wipe(byteRepresentation); char[] newCharRepresentation = new char[length+1]; for(int i=0; i<this.length; i++) newCharRepresentation[i] = charRepresentation[i]; newCharRepresentation[length-1] = c; wipe(charRepresentation); initialise(newCharRepresentation); } /** * Internal method to wipe a char[] * @param chars */ private void wipe(char[] chars){ for(int i=0; i<chars.length; i++){ //set to hex AA int val = 0xAA; chars[i] = (char) val; //set to hex 55 val = 0x55; chars[i] = (char) val; //set to hex 00 val = 0x00; chars[i] = (char) val; } } /** * Internal method to wipe a byte[] * @param chars */ private void wipe(byte[] bytes){ for(int i=0; i<bytes.length; i++){ //set to hex AA int val = 0xAA; bytes[i] = (byte) val; //set to hex 55 val = 0x55; bytes[i] = (byte) val; //set to hex 00 val = 0x00; bytes[i] = (byte) val; } } /** * Safely wipes all the data */ public void destroy(){ if(this.isDestroyed) return; wipe(charRepresentation); wipe(charRepresentationLower); wipe(byteRepresentation); //loose references charRepresentation = null; charRepresentationLower = null; byteRepresentation = null; this.length = 0; //TODO: call garbage collector? //Runtime.getRuntime().gc(); this.isDestroyed = true; } /** * This method is only to check if non-sensitive data * is part of the SecureString * @return true if SecureString contains the given String */ public boolean contains(String nonSensitiveData){ if(this.isDestroyed) return false; if(nonSensitiveData.length() > this.length) return false; char[] nonSensitiveDataChars = nonSensitiveData.toCharArray(); int positionInNonSensitiveData = 0; for(int i=0; i < this.length; i++){ if(positionInNonSensitiveData == nonSensitiveDataChars.length) return true; if(this.charRepresentation[i] == nonSensitiveDataChars[positionInNonSensitiveData]) positionInNonSensitiveData++; else positionInNonSensitiveData = 0; } return false; } }
Does that make any sense? How does the Java VM handle this? And how does the Android Dalvik Java VM handle this? Does the Android system cache the UI inputs anyway (so there would be no point in wiping our copies)? Is the data really going to be wiped when I use this class? Where are the real Java internals heroes out there?
Update 1, 11th September 2013: Added Beerware license. Note: This was just some idea I had. Use at your own risk.