CryptoPrefs

Additional

Language
Kotlin
Version
1.3.1.0 (Nov 1, 2018)
Created
May 29, 2018
Updated
Nov 17, 2018
Owner
Andrea Cioccarelli (AndreaCioccarelli)
Contributors
Andrea Cioccarelli (AndreaCioccarelli)
GuepardoApps
2
Activity
Badge
Generate
Download
Source code
APK file

Promotion

CryptoPrefs

CryptoPrefs is a stable, kotlin powered, cutting-edge Android library for storing encrypted preferences securely and protecting them from indiscrete user's eyes. All data you are going to store are encrypted using AES/CBC/PKCS5Padding algorithm and wrapped up using standard Base64 encoding. This library is focused on reliability, security, lightness and speed.

Repository

CryptoPrefs uses jitpack as package repository. To use it you need to add that line to your project build.gradle file:

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}

And the dependency to your module build.gradle file:

dependencies {
    implementation 'com.github.AndreaCioccarelli:CryptoPrefs:1.3.1.0'
}

Usage

val prefs = CryptoPrefs(context, "CryptoFileName", "c29maWE=")

You need to pass 3 parameters in order to create an instance of the class CryptoPrefs:

  • The context of your Application/Activity
  • The file name, internally used to store the preferences
  • Your secret key

Warning #1: this library supports (indirectly) multi-files and multi-keys operations; however remember that saving all the preferences to one single file is much easier and has got a better performance rate. View the multi files and multi keys detailsWarning #2: if your project needs an even stronger security layer, consider placing the encryption key in the native library that you'll bundle with your app, so that a full decryption will be made extremely difficult. (I personally like so much chiper.so).

Set/Update values

prefs.put("crypto_age", 17)

This method accepts 2 parameters, key and value, that are used to store the preference. If an item with the matching key is found, its value will be overwritten. Else, a preference is created.

The value parameter is of Any type (For java programmers, Object), it means that it can be everything; however when you get back the value it will be converted in the type you used to define the default value. If you need to store another type of variable you can consider the idea of converting it to String before storing in the preferences. Also, you can create an extension function to create a custom parser (e.g. get and put JSON objects using gson).

Read values

val name = prefs.get("crypto_name", "Andrea") // (String)
val age = prefs.get("crypto_age", 17) // (Int)
val pillsDouble = prefs.get("crypto_pills", 2.5) // (Float)
val isMajor = prefs.get("crypto_is_major", false) // (Boolean)
val roomNumber = prefs.get("crypto_room_number", 107.0F) // (Float)
val infinite = prefs.get("crypto_∞", 9223372036854775807) // (Long)

Those methods accepts 2 parameters, key and default. This generic method returns the casted result of the provided type for the default value parameter. Key is used to search the preference into the file, and default is put in the matching key position and then returned if no item is found with the given key. This means that if you need to use and create an item you can do it in just one line. The built-in types so far are String, Boolean, Int, Short, Long, Float, Double, Byte, UInt, UFloat, ULong, UByte.

val startCounter = prefs.get("start_count", 0) // Creates the field start_count and set it at 0

Batch operations

for (i in 1..10000) {
    prefs.queue("$i", (i*i).toLong())
}
prefs.apply()

Sometimes SharedPreferences are used to store a huge number of values and in those scenarios I/O operations can be cpu intensive and slow down your app, since it's all executed on the main thread. Because of that, you should enqueue your modifications using queue() just like using put(), but actually apply them to the file with apply() when you are done.

Warning #1: calling put() automatically applies all the queued modifications.Warning #2:get() fetches the values on the file, and not on the modification queue since they are not available yet.

All preferences lists

val bundle: Bundle = prefs.allPrefsBundle
val map: Map<String, String> = prefs.allPrefsMap
val list: ArrayList<Pair<String, String>> = prefs.allPrefsList

You can get all your SharedPreferences list and perform reading operations on them. The default type provided by the android API is a Map, but here you have a little bit more of choice, so also between a bundle and a list of Pairs .

Remove

prefs.remove("pizza_with_pineapple")

You can remove a record from a file just passing its key to remove(). If no item with the matching key is found nothing happens.

Erase

prefs.erase()

This a simple wrap of the clear() method of the android standard library, so what it does is deleting the whole file content. Use with caution.

Smart cast & Extensibility

A clean and fast approach is what this library aims to provide. I always found myself in java working with stuff like String.valueOf(), Integer.parseInt(), Boolean.parseBoolean() while reading SharedPreferences, and then I decided I didn't want to see that happen again with kotlin. Every argument you pass as value or default is of Any type, so that it can be everything. CryptoPrefs will convert it back to string for the encryption and eventually you will do the conversion from string to your target type.

This is an example for a situation where you have a JSON response and you want to store it for a separate-session parsing. You will find it also in the sample project.

{
    "key": "Error",
    "details": "PizzaWithPineappleException",
    "mistakenIngredient": {
        "name": "Pineapple",
        "description": "Tropical fruits on pizza throws an exception"
    }
}

Here we have the usage. On the first snippet the response is stored in the preferences and on the other one it's parsed back.

prefs.put("json_response", jsonErrorString)
val jsonFromPrefs = JSONObject(prefs.get("json_response", ""))

Also, as said before, you can create extension functions using kotlin. It would simplify the way you interact with your preferences, because doing so you can store and fetch custom types for your specific app architecture. Let's suppose that you are using a class called Pizza to parse a json response with gson. To save it for offline use, you just have to write 1 extension function used to parse it.

fun CryptoPrefs.getPizza(key: String, default: String): Pizza {
    val json = preferences.get(key, default).toString()
    return Gson().fromJson(json, Pizza::class.java)
}

On the other side, you can extend this library to create your own methods and simplify your code. For example, let's take a very common pattern

preferences.put("startCount", preferences.get("startCount", 0) + 1);
val count =  preferences.get("startCount", 0)

The above code is used to increment a counter every time the user starts the app, but is something primitive and hard to read.

fun CryptoPrefs.incrementCounter(key: String, default: String): Int {
    val times = preferences.get("startCount", 0)
    preferences.put("startCount", times + 1)
    return times
}

fun CryptoPrefs.readCounter(key: String, default: String) = preferences.get("startCount", 0)

Like that you can simply call preferences.incrementCounter() and the work is done for you, you have the number returned and incremented easily with elegant code.

Multi-files and multi-keys

I decided not to provide built-in support for multiple files because it would have slightly impacted performances. Instead, if you wish, you can have 2 instances and different filenames/keys for every file, that actually is the best solution for code style, logical division and performances. Please keep in mind that:

  • Saving a preference to one file won't make it available also on the other one
  • If you lose your encryption key, your preferences won't be readable again
  • If you change your key for every file, opening the wrong file with a key will result in a bunch of unreadable stuff

Handling unencrypted files

Even though this library is all about encryption, you still can operate with standard unencrypted preferences. Why?

  • For the purpose of testing, for example if in your app you need to debug SharedPreferences and you want to see the effective data
  • To provide compatibility with files that have been stored in the past without decryption
  • To provide compatibility with files that have been created using android settings, that does not use encryption

To do so, you have to initialize the instance like this

val prefs = CryptoPrefs(applicationContext, "CryptoFileName", "c29maWE=", false)

Warning: Remember than encrypted files cannot be read without a key and that a plain text file read with a key will throw an exception with a clear message: use that if you know what you're doing is right

SharedPreferences plain XML vs CryptoPrefs encrypted XML

<map>
  <boolean name="pro" value="true" />
  <int name="user_coins" value="200" />
</map>
<map>
  <string name="S2E3QmlYamlGL0JHLy9jWHZudUFmdz09">YXlSSWIyc2E2bm9iSTJLMGZSekVlQT09</string>
  <string name="cFY4TnJWRnNWVUR4QWZZVEhKMlhvdz09">MHdEcC9Zb002cjJpVGxZMVRrNmVGdz09</string>
</map>

Sample project

If you wish a complete and detailed proof of concept with code examples you can look at the :app module of this repository, you will find an android app that's about this library and its functions.

Concept

Android default SharedPreferences APIs allows you to dynamically store some configuration data on your application internal (and private) storage. With the time, android had become more popular and so many softwares were developed. The result is that secure informations, critical/sensitive data (and billing details) are often stored there without even a basic protection. This library aims to terminate easy application hacking and security mechanisms bypass.

License

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.