Wednesday, September 7, 2011

Protecting Passwords in Android Applications

Ok, so your Android app needs to collect a user name and password for access to a remote system.  Most likely you have a preferences.xml file like this:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
  xmlns:android="http://schemas.android.com/apk/res/android">
    <EditTextPreference
        android:title="User"
        android:key="userId" android:summary="@string/summary_user"></EditTextPreference>
        
    <EditTextPreference android:title="Password"
        android:key="password" android:password="true"  android:summary="@string/summary_password"/>
        
</PreferenceScreen> 

You dutifully add the android:password="true" attribute in order to have Android treat the value with the proper respect. Then you add your preference Activity, like this
public class FooPreferenceActivity extends PreferenceActivity {

	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		addPreferencesFromResource(R.xml.prefs);
	}
	
}
Works great. But there is a big problem. Take a look at the shared preferences file that Android writes on the device.  Android hasn't really given your password the proper respect.  It's stored in clear text.

Yes, I know, other processes on the device can't normally access the file, but let's not naively assume that such protection really means the password is safe. When your client's lost phone gets rooted, they won't be happy with you if you left their passwords in the clear.

Assuming our package is "com.foo", you can get a look at the preferences with adb like this:

adb pull /data/data/com.foo/shared_prefs/com.foo_preferences.xml

So, how do we get Android to encrypt the password?  Well, maybe there is a way, but I couldn't find it.  And maybe this is a totally stupid newbie trick, but here's how I solved the problem:

I created my own PasswordEditTextPreference for Android to use.  The preferences xml now looks like this:


<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
  xmlns:android="http://schemas.android.com/apk/res/android">
    <EditTextPreference
        android:title="User"
        android:key="userId" android:summary="@string/summary_user"></EditTextPreference>
        
    <PasswordEditTextPreference android:title="Password"
        android:key="password" android:password="true"  android:summary="@string/summary_password"/>
        
</PreferenceScreen> 
Then I created the PasswordEditTextPreference class, which extends EditTextPreference, and overrides methods to hook into the preference handling process and encrypt/decrypt the value in the text view on load/save. Here's what the code looks like:
package android.preference;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;

public class PasswordEditTextPreference extends EditTextPreference {

	private PreferenceObfuscator obfuscator = new PreferenceObfuscator();
	
	public PasswordEditTextPreference(Context context) {
		super(context);
	}

	public PasswordEditTextPreference(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}

	public PasswordEditTextPreference(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	
	@Override
	protected void onDialogClosed(boolean positiveResult) {
		super.getEditText().setText(obfuscator.encrypt(getEditText().getText().toString()));
		super.onDialogClosed(positiveResult);
	}
	
	@Override
	protected void onBindDialogView(View view) {
		super.onBindDialogView(view);
		this.getEditText().setText(obfuscator.decrypt(getEditText().getText().toString()));
	}
}

Notice that this class is in the android.preference package. I don't think this will work if you don't  put it in that package. Also, I have not explored whether all of the constructors are necessary, but  I overrode them to ensure full substitutability with EditTextPreference. The onBindDialogView method decrypts the stored value as it is getting put into the preference edit dialog. The onDialogClosed method then encrypts the value when the user dismisses the dialog.

Strictly speaking, encryption only needs to be done if the "positiveResult" parameter is true. I thought for sure something would complain at me for doing this -- specifically the foreign XML element in the preferences.xml. But it's working great. No more clear-text passwords.

5 comments:

  1. Hey,
    Nice but ??? this does NOT work...
    Where did you get the PreferenceObfuscator object from???
    Tried and Eclipse does not recognize this as an import...
    "-)
    It would be nice if you could help...

    ReplyDelete
  2. Hi gamerOne7,

    You just write you own using the javax.crypto classes. In case you're unfamiliar with that, here's an example. This uses a HexEncoder class because Android platform didn't start supplying a Base64 encoder until API version 8 (if memory serves). If you're using API>8, I would replace the hex encoding with Base64 encoding. It's preferred anyway.:

    import java.security.SecureRandom;

    import javax.crypto.Cipher;
    import javax.crypto.KeyGenerator;
    import javax.crypto.SecretKey;
    import javax.crypto.spec.SecretKeySpec;



    public class PreferenceObfuscator {

    // this is just a bogus key value for demonstration only. You should replace this with your own random secret bytes.
    private static final byte[] _B = "bbbbbbbbbb bbbbbbbbbb bbbbbbbbbb bbbbbbbbbb".getBytes();

    public String encrypt(String cleartext) throws Exception {
    return HexEncoder.toHex(encrypt(getKey(_B), cleartext));
    }

    public String decrypt(String encrypted) throws Exception {
    return new String(decrypt(getKey(_B), HexEncoder.fromHex(encrypted)));
    }

    private byte[] getKey(byte[] seed) throws Exception {
    SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
    secureRandom.setSeed(seed);
    KeyGenerator kgen = KeyGenerator.getInstance("AES");
    kgen.init(128, secureRandom);
    SecretKey skey = kgen.generateKey();
    return skey.getEncoded();
    }

    private byte[] encrypt(byte[] raw, String clear) throws Exception {
    SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
    return cipher.doFinal(clear.getBytes());
    }

    private byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {
    SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.DECRYPT_MODE, skeySpec);
    return cipher.doFinal(encrypted);
    }

    }

    ReplyDelete
  3. Thanx a milliom...
    I shall givit a try...
    Sorry for ignorance...
    :-)

    ReplyDelete
  4. Thank you Eric
    I will give it a try following you lead...
    Cheers :-)

    ReplyDelete