Storing Preferences Securely on Android

http://justmobiledev.com/wp-content/uploads/2018/12/android_apps_flag_bookeh.jpgStoring Preferences Securely on Android

Why store preferences securely?

You may wonder why this blog post on storing preferences securely so here it goes: For most apps preferences can contain non-confidential information such as which theme the user prefers and if they would like to see notifications from the app. Things like that. This type of information is non-confidential and usually does not require protection.

However, if you have developed an app, you know that the Android SharedPreferences are a quick and convenient option to store persistent data. Data that is accessible after the app restarts after a force stop and after an upgrade.

For most apps I have developed, the team ends up storing data in the preferences that may be useful for a hacker to exploit the app, for example user credentials, the user’s private data, or credentials for client-server information. These pieces of information need to be protected and that’s what this article is about.

How can you store non-secure preferences?

Android SharedPreferences class provides a convenient interface to store and retrieve preferences private to the app. It has utility methods to check if a preference exists and getter methods for the different data types, e.g. int, string, long, boolean.

The way you use the SharedPreference class is to create and editor that initializes the SharedPreferences with a identifier for these preferences and then use the put- and get- methods in combination with the editor to store and retrieve the preferences.

How to use SharedPreferences

Here a quick recap on how you use the normal Android SharedPreferences. First you get the SharedPreferences by passing in a name that you would like to use. Then you create and editor for these preferences and use the editor put methods to add your data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
final String MY_APP_PREFERENCES = "my_app_preferences";

try{
SharedPreferences pref = getApplicationContext().getSharedPreferences(MY_APP_PREFERENCES, 0); // 0 - for private mode
SharedPreferences.Editor editor = pref.edit();

// Storing boolean - true/false
editor.putBoolean("my_boolean_key", true);
// Storing a string
editor.putString("my_string_key", "string value");
// Storing and integer
editor.putInt("my_int_key", 10001);
// Storing float
editor.putFloat("my_float_key", 17.1111f);
// Storing a long
editor.putLong("my_long_key", 100000000);

editor.commit(); // commit changes

}
catch(Exception ex){
Log.e(TAG, ex.getMessage());
}

Retrieving data from SharedPreferences

Retrieving data from the SharedPreferences works easy by using the preference instance and the SharedPreference get methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final String MY_APP_PREFERENCES = "my_app_preferences";

try{
SharedPreferences pref = getApplicationContext().getSharedPreferences(MY_APP_PREFERENCES, 0); // 0 - for private mode
SharedPreferences.Editor editor = pref.edit();

// Retrieve boolean - true/false
boolean myBoolean = pref.getBoolean("my_boolean_key", false);
// Retrieve a string
String myString = pref.getString("my_string_key", "");
// Retrieve and integer
int myInt = pref.getInt("my_int_key", -1);
// Retrieve float
float myFloat = pref.getFloat("my_float_key", -1);
// Retrieve a long
long myLong = pref.getLong("my_long_key", -1);
}
catch(Exception ex){
Log.e(TAG, ex.getMessage());
}

How are SharedPreferences stored?

SharedPreferences are stored in an xml file in the app data folder, i.e.
/data/data/YOUR_PACKAGE_NAME/shared_prefs/YOUR_PREFS_NAME.xml
or the default preferences at:
/data/data/YOUR_PACKAGE_NAME/shared_prefs/YOUR_PACKAGE_NAME_preferences.xml

In other words, if a hacker obtains access to to a user’s device, he can easily extract the stored preferences from the device once it is unlocked.

Storing Preferences Securely

So what are the alternatives to store preferences securely? There are several:

  1. You could develop a custom class that encrypts the preferences with an encryption key stored in the Key Store using a sufficiently strong encryption algorithm such as AES-256 CBC. The encrypted values could be stored in the SharedPreferences.
  2. You could also store all preferences in a database table and use a database encryption module such as SQLCipher or Realm encryption. In this make sure that the encryption key is generated in the Android Keystore at first app start and not hard-coded in your source code.
  3. You could also serialize the preferences from an object into a flat file stored in the app private storage. Then you could encrypt the entire file and store the encryption key in the Android Keystore.
  4. Use a third-party library such as ‘Scottyab Secure Preferences’ which provides a transparent encryption interface to the SharedPreferences.

Options, options… Which one you end up choosing depends on a variety of factors, such as the level of security you are targeting, your Android expertise, number of preferences stored, desire for convenience, etc.

In this blog post, I’d like to select a balance between security and convenience and go with the ophio-secure library.

Secure Preferences Libraries

I wouldn’t recommend another SharedPreference wrapper called Ophio Secure since it uses a weak encryption algorithm: Password Based Encryption with DES, which only has a key size of 56 bits.

Another Secure Preferences package called ‘Scottyab Secure Preferences’  provides a relatively strong encryption algorithm, which is AES 128 bit keys with CBC Mode and PKCS5 padding

Secure Preferences Implementation

Instead of using one of the existing alternatives, I decide to do my own implementation with the following requirements:

  1. Encryption keys should be stored in the Android Key Store.
  2. Both keys and values of the preference items should be encrypted.
  3. Leverage the Android standard SharedPreferences class
  4. Use a sufficiently strong encryption algorithm like AES-256 CBC mode.

SecurePrefsBuilder

The SecurePrefsBuilder is used to build the secure preferences based on a set of configuration parameters such as obfuscateKey and obfuscateValue. Then the builder creates the actual SharedPreference instance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import android.app.Application;
import android.content.SharedPreferences;

public class SecurePrefsBuilder {
    private Application application;
    private String sharedPrefFileName = "my_app_shared_prefs";
    private boolean isObfuscated;
    private boolean isKeyObfuscated = true;

    public SecurePrefsBuilder() {
    }

    public SecurePrefsBuilder setApplication(Application application) {
        this.application = application;
        return this;
    }

    public SecurePrefsBuilder obfuscateValue(boolean obfuscated) {
        this.isObfuscated = obfuscated;
        return this;
    }

    public SecurePrefsBuilder obfuscateKey(boolean obfuscateKey) {
        this.isKeyObfuscated = obfuscateKey;
        return this;
    }

    public SecurePrefsBuilder setSharePrefFileName(String fileName) {
        this.sharedPrefFileName = fileName;
        return this;
    }

    public SharedPreferences createSharedPrefs() {
        SharedPreferences sharedPrefDelegate = this.application.getSharedPreferences(this.sharedPrefFileName, 0);
        Object sharedPreferences;
        if (!this.isObfuscated && !this.isKeyObfuscated) {
            sharedPreferences = sharedPrefDelegate;
        } else {
            sharedPreferences = new SecurePrefs(this.application, sharedPrefDelegate, this.isKeyObfuscated);
        }

        return (SharedPreferences)sharedPreferences;
    }
}

SecurePrefs 

The SecurePrefs class is a sub-class of the Android SharedPreferences and thus inherits all it’s functionality. As you can see we are overriding the Getter and Setter classes to encrypt values being stored and retrieved in the preferences.

We are also using a delegate implementation to encrypt and decrypt keys that are used to look up values in the preferences as well as the values themselves.

When the SecurePrefs class is instantiated, we create a new secret key for AES-256 encryption in the Android Key Store. This key is used in the encrypt() and decrypt() methods by retrieving it from the key store.

Whenever a key and value is put or retrieved from the preferences, both key and value go through the encryption and decryption steps and then the results are converted to Base64 and stored in the regular SharedPreferences. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
import android.content.Context;
import android.content.SharedPreferences;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.util.Log;

import java.security.KeyStore;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

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

public class SecurePrefs implements SharedPreferences {
    private final String TAG = "SecurePrefs";

    // Constants
    private static final String ANDROID_KEYSTORE_PROVIDER = "AndroidKeyStore";
    private static final String SECURE_PREF_ALIAS_KEY = "SecurePreferencesKey";
    protected static final String UTF8_ENCODING = "utf-8";
    private final String AES_CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding";
    // NOTE: Change to a different IV
    private final byte[] IV = "WtiuyuucMPaQ9kVV".getBytes();

    // Private members
    private boolean encryptKeys = true;
    private SharedPreferences delegate;
    private Context context;


    public SecurePrefs(Context context, SharedPreferences delegate, boolean encryptKeys) {
        this.delegate = delegate;
        this.context = context;
        this.encryptKeys = encryptKeys;

        createEncryptionKey();
    }

    private Set<String> encryptSet(Set<String> values) {
        Set<String> encryptedValues = new HashSet<String>();
        for (String value : values) {
            encryptedValues.add(encrypt(value));
        }
        return encryptedValues;
    }

    private Set<String> decryptSet(Set<String> values) {
        Set<String> decryptedValues = new HashSet<String>();
        for (String value : values) {
            decryptedValues.add(decrypt(value));
        }
        return decryptedValues;
    }

    private String encryptKey(String key) {
        return encryptKeys ? encrypt(key) : key;
    }

    public SecurePrefs.Editor edit() {
        return new SecurePrefs.Editor();
    }

    @Override
    public Map<String, String> getAll() {
        Map<String, ?> all = delegate.getAll();
        Set<String> keys = all.keySet();
        HashMap<String, String> unencryptedMap = new HashMap<>(keys.size());
        for (String key : keys) {
            String decryptedKey = decryptKey(key);
            Object value = all.get(key);
            if (value != null) {
                unencryptedMap.put(decryptedKey, decrypt(value.toString()));
            }
        }
        return unencryptedMap;
    }

    private String decryptKey(String key) {
        if (encryptKeys) {
            return decrypt(key);
        }
        return key;
    }

    @Override
    public boolean getBoolean(String key, boolean defValue) {
        final String v = delegate.getString(encryptKey(key), null);
        return v != null ? Boolean.parseBoolean(decrypt(v)) : defValue;
    }

    @Override
    public float getFloat(String key, float defValue) {
        final String v = delegate.getString(encryptKey(key), null);
        return v != null ? Float.parseFloat(decrypt(v)) : defValue;
    }

    @Override
    public int getInt(String key, int defValue) {
        final String v = delegate.getString(encryptKey(key), null);
        return v != null ? Integer.parseInt(decrypt(v)) : defValue;
    }

    @Override
    public long getLong(String key, long defValue) {
        final String v = delegate.getString(encryptKey(key), null);
        return v != null ? Long.parseLong(decrypt(v)) : defValue;
    }

    @Override
    public String getString(String key, String defValue) {
        final String v = delegate.getString(encryptKey(key), null);
        return v != null ? decrypt(v) : defValue;
    }

    @Override
    public Set<String> getStringSet(String key, Set<String> defValues) {
        final Set<String> stringSet = delegate.getStringSet(encryptKey(key), defValues);
        return stringSet != null ? decryptSet(stringSet) : defValues;
    }

    @Override
    public boolean contains(String s) {
        s = encryptKey(s);
        return delegate.contains(s);
    }

    @Override
    public void registerOnSharedPreferenceChangeListener(
            OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
        delegate.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
    }

    @Override
    public void unregisterOnSharedPreferenceChangeListener(
            OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
        delegate.unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
    }

    public class Editor implements SharedPreferences.Editor {

        protected SharedPreferences.Editor delegate;

        public Editor() {
            this.delegate = SecurePrefs.this.delegate.edit();
        }

        @Override
        public SecurePrefs.Editor putBoolean(String key, boolean value) {
            delegate.putString(encryptKey(key), encrypt(Boolean.toString(value)));
            return this;
        }

        @Override
        public SecurePrefs.Editor putFloat(String key, float value) {
            delegate.putString(encryptKey(key), encrypt(Float.toString(value)));
            return this;
        }

        @Override
        public SecurePrefs.Editor putInt(String key, int value) {
            delegate.putString(encryptKey(key), encrypt(Integer.toString(value)));
            return this;
        }

        @Override
        public SecurePrefs.Editor putLong(String key, long value) {
            delegate.putString(encryptKey(key), encrypt(Long.toString(value)));
            return this;
        }

        @Override
        public SecurePrefs.Editor putString(String key, String value) {
            delegate.putString(encryptKey(key), encrypt(value));
            return this;
        }

        @Override
        public SharedPreferences.Editor putStringSet(String key, Set<String> values) {
            delegate.putStringSet(encryptKey(key), encryptSet(values));
            return this;
        }

        @Override
        public void apply() {
            delegate.apply();
        }

        @Override
        public SecurePrefs.Editor clear() {
            delegate.clear();
            return this;
        }

        @Override
        public boolean commit() {
            return delegate.commit();
        }

        @Override
        public SecurePrefs.Editor remove(String s) {
            delegate.remove(encryptKey(s));
            return this;
        }
    }

    private void createEncryptionKey(){
        try{
            KeyGenerator keyGenerator = KeyGenerator.getInstance(
                    KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE_PROVIDER);
            keyGenerator.init(
                    new KeyGenParameterSpec.Builder(SECURE_PREF_ALIAS_KEY,
                            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                            .setKeySize(256)
                            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
                            .setRandomizedEncryptionRequired(false)
                            .build());
            keyGenerator.generateKey();
        }
        catch(Exception ex){
            Log.e(TAG, ex.getLocalizedMessage());
        }
    }

    private SecretKey getSecretKeyFromKeyStore(){
        SecretKey secretKey = null;

        try{
            KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE_PROVIDER);
            keyStore.load(null);
            KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(SECURE_PREF_ALIAS_KEY, null);
            if (secretKeyEntry == null) {
                createEncryptionKey();
                secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(SECURE_PREF_ALIAS_KEY, null);
            }

            secretKey = secretKeyEntry.getSecretKey();
        }
        catch(Exception ex){
            Log.e(TAG, ex.getLocalizedMessage());
        }

        return secretKey;
    }

    protected String encrypt(String value) {
        String encryptedStr = "";

        try {

            // Get key from key store
            SecretKey secretKey = getSecretKeyFromKeyStore();

            // Initialize AES
            Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(IV);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);

            // Encrypt
            byte[] encryptedPasswordBytes = cipher.doFinal(value.getBytes(UTF8_ENCODING));

            // Convert to Base 64
            encryptedStr = Base64.encodeToString(encryptedPasswordBytes, Base64.DEFAULT);

        } catch (Exception ex) {
            Log.e(TAG, ex.getLocalizedMessage());
        }

        return encryptedStr;
    }


    protected String decrypt(String value) {
        String decryptedStr = "";

        try {
            // Get key from key store
            SecretKey secretKey = getSecretKeyFromKeyStore();

            // Initialize AES
            Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(IV);
            cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);

            // Convert from Base 64
            byte[] encryptedPasswordBytes = Base64.decode(value, Base64.DEFAULT);

            byte[] decryptedPasswordBytes = cipher.doFinal(encryptedPasswordBytes);

            // Convert Bytes to String
            decryptedStr = new String(decryptedPasswordBytes, "UTF8");

        } catch (Exception e){
            e.printStackTrace();
        }
        return decryptedStr;
    }

Using the SecretPrefs

Using the SecretPrefs class works just like you would use the SharedPreferences. You use the SecretPrefs builder class to build the preferences and then an editor to add values.

Don’t forget the .commit() command to complete adding your values. Then you can use the preferences to retrieve the values again.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
        SharedPreferences securePreferences = new SecurePrefsBuilder()
                .setApplication(MyApplication.getInstance())
                .obfuscateValue(true)
                .obfuscateKey(true)
                .setSharePrefFileName(SECURE_PREFS_FILE_NAME)
                .createSharedPrefs();

        // Get an editor
        SharedPreferences.Editor editor = securePreferences.edit();

        editor.putString(SECURE_PREFS_STRING_KEY, "this is my secret string");
        editor.commit();

        String myString = securePreferences.getString(SECURE_PREFS_STRING_KEY, "");

Implementation, Library, Sample App

To see all the pieces working together, check out my library and sample app (Java) on my GitHub repository here.