Making secured version of EditTextPreference
Sometimes it's needed to add an additional protection level for the data, which is stored in shared preferences. What's the easy way to do that? In assumption, that in most cases data to store is coming from PreferenceFragment, you can simply extend EditTextPreference class to to make a secure implementation. Suggested approach is based on a middleware filter, which encrypts\decrypts all the data coming from/into EditTextPreference
. There is a nice post on SO, which show how EditTextPreference
should be extended, but in this post I'll show a possible implementation of middleware filter. Let's start from the following:
public class SecuredEditTextPreference extends EditTextPreference {
public SecuredEditTextPreference(Context context) {
super(context);
}
public SecuredEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SecuredEditTextPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
super.setText(restoreValue ? getPersistedString(null) : (String) defaultValue);
}
@Override
public void setText(String text) {
if (text == null || text.length() == 0) super.setText(text);
super.setText(SecurityManager
.getInstance(getContext())
.encryptString(text));
}
@Override
public String getText() {
String text = super.getText();
if (text == null || text.length() == 0) return text;
return SecurityManager
.getInstance(getContext())
.decryptString(text);
}
}
Everything is pretty straightforward: getText
and setText
methods are overridden to use SecurityManager
class as encryption-decryption filter, which is implemented in the following way:
public class SecurityManager {
private static SecurityManager sInstance;
private SecretKey mKey;
private Context mContext;
private SecurityManager(Context context) {
String androidId = Settings
.Secure
.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
mContext = context.getApplicationContext();
try {
byte[] key = androidId.getBytes("UTF8");
MessageDigest sha = MessageDigest.getInstance("SHA-1");
key = sha.digest(key);
key = Arrays.copyOf(key, 16);
mKey = new SecretKeySpec(key, "AES");
} catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
public static SecurityManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new SecurityManager(context);
}
return sInstance;
}
public String encryptString(String stringToEncrypt) {
String output = stringToEncrypt;
try {
byte[] clearText = stringToEncrypt.getBytes("UTF8");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, mKey);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("iv", Base64.encodeToString(cipher.getIV(), Base64.NO_WRAP));
editor.apply();
output = new String(Base64.encode(cipher.doFinal(clearText),
Base64.NO_WRAP), "UTF8");
} catch (UnsupportedEncodingException | NoSuchAlgorithmException |
NoSuchPaddingException | InvalidKeyException |
IllegalBlockSizeException | BadPaddingException e) {
e.printStackTrace();
}
return output;
}
public String decryptString(String stringToDecrypt) {
String output = stringToDecrypt;
try {
byte[] encryptedBytes = Base64.decode(stringToDecrypt, Base64.DEFAULT);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
byte[] iv = Base64.decode(prefs.getString("iv", ""), Base64.NO_WRAP);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, mKey, ivSpec);
output = new String(cipher.doFinal(encryptedBytes), "UTF8");
} catch (NoSuchAlgorithmException | NoSuchPaddingException
| InvalidKeyException | UnsupportedEncodingException
| IllegalBlockSizeException | BadPaddingException
| InvalidAlgorithmParameterException e) {
e.printStackTrace();
}
return output;
}
}
At the constructor call new AES
key is created by using ANDROID_ID which is unique for every user:
A 64-bit number (as a hex string) that is randomly generated when the user first sets up the device and should remain constant for the lifetime of the user's device.
In addition to unique nature of this value there is one more advantage - no permissions are needed to get it. However, it has some bugs, so you can use another data as the key. SHA-1
hash calculation is used to guarantee, that array will be more than 16 bytes (128 bits) since AES
key should be 128-bit long (192 and 256-bit keys are not supported out of the box). SHA-1
hash has size of 20 bytes (160 bits), so we should take only first 16 bytes.
To encrypt/decrypt string you should get an instance of Cipher
object by providing transformation string. It can be in one of the following formats: algorithm/mode/padding
or algorithm
. By specifying only algorithm
, provider-specific value for mode
and padding
are used. Default mode for AES
is ECB
, which is less secure, so it's better to specify CBC
. By using CBC
mode you should also provide Initialization Vector, it will be automatically generated at the encryption; you should store it somewhere, since it should be used for decryption, it can be written to the same shared preferences as encrypted text. After encryption, we will get the values like the following:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="secured_preference">ekjejxO20DOFDeAzeD2K9Q==</string>
<string name="iv">oJZ0WNt3J2NNvsltv6+YGA==</string>
</map>
The important note on this approach is that it is not complete security solution, it's just an additional protection level, which can be improved or extended depending on your needs.