Secure Your Data with Android Secure Persist Library

Ioannis Anifantakis
6 min readJul 3, 2024

--

Leverage Your Persistence and Encryption in Android

by Ioannis Anifantakis

As mobile applications increasingly handle sensitive user data, ensuring the security of this information is paramount. The Android Secure Persist Library is a tool designed to provide secure and efficient storage of preferences in Android applications. Written in Kotlin, this library leverages the Android KeyStore and modern encryption techniques to protect data from unauthorized access.

Key Features

  • Secure Preferences Management: Easily encrypt and decrypt preferences using SharedPreferences and DataStore.
  • Property Delegation: Seamlessly integrate encrypted preferences with Kotlin property delegation.
  • Raw Data Encryption: Directly encrypt and decrypt raw data with EncryptionManager.
  • Asynchronous Operations: Handle preferences efficiently with non-blocking operations using DataStore.

Why Use SecurePersist?

  • Security: Protect sensitive data with robust encryption.
  • Ease of Use: Manage encrypted preferences effortlessly with a user-friendly API.
  • Versatility: Support various data types and integrate seamlessly with Android components.
  • Performance: Ensure non-blocking operations for a smooth user experience.

Getting Started

Installation

There are two ways to install the library, directly as a module to your application or by adding it as a jitpack dependency.

Please refer to the repository README file that contains detailed directions about how to install this library.

Using SecurePersist Library

SecurePerist Library consists of PersistManager and EncryptionManager.

PersistManager uses the EncryptionManager to complement it with encrypted functionalities behind the scenes to encrypt and decrypt data for SharedPreferences and DataStore.

EncryptionManager on the other hand can be used independently to additionally provide encryption and decryption for raw data that doesn’t depend on PersistManager.

PersistManager

PersistManager is the core component, managing encrypted preferences using SharedPreferences and DataStore.

PersistManager is the core component of SecurePersist. It manages encrypted preferences using both SharedPreferences and DataStore leveraging the EncryptionManager's cryptographic algorithms.

Initialization

During the initialization of Persist Manager, it also creates an instance of its own EncryptionManager to manage encryption and decryption of persist data. If you don’t need to encrypt and decrypt external data, other than SharedPreferences and DataStore Preferences, then you don’t need to make an EncryptionManager instance of its own.

(No need to say it is better to use a DI framework like Hilt and provide persistManager instance as a singleton injection — See library’s README at GitHub)

// create a PersistManager instance with custom KeyStore alias
val persistManager = PersistManager(context, "your_key_alias")
// create a PersistManager instance with "keyAlias" as default KeyStore alias
val persistManager = PersistManager(context)

SharedPreferences Encryption

Android Secure Persist offers a zero-configuration approach for encrypting and decrypting SharedPreferences. This means you can easily secure your SharedPreferences without additional setup.

SharedPreferences Example

At the following example we encrypt a shared preference with key “key1” and value “secureValue”.

Then we ask persistManager to decrypt the encrypted value held by “key1”. If “key1” is undefined, then “defaultValue” will be returned.

Then we can delete a shared preference that has key “key1”.

// Encrypt and save a preference
// with key "key1" and value to encrypt is "secureValue"
persistManager.encryptSharedPreference("key1", "secureValue")

// Decrypt and retrieve a preference
val value: String = persistManager.decryptSharedPreference("key1", "defaultValue")

// Delete a preference
persistManager.deleteSharedPreference("key1")

SharedPreferences Property Delegation

This library allows you to use Kotlin property delegation for encrypted SharedPreferences, providing a clean and intuitive way to handle your encrypted preferences.

Use encrypted SharedPreferences as normal variables using property delegation

Delegation example with manual key

In this first example you can set the key of the sharedPreference manually, where we specify “secureString” as the key-name.

// create
var myPreference by persistManager.preference("secureString", "default")

myPreference = "newSecureValue"
val storedValue = myPreference

Delegation example with automatic key

But with Property Delegation this is the preferred way, where you don’t neeed to specify key, the variable name will be the key-name behind the scenes, so here “secureString” will be the key-name because that is the name of the variable itself.

// create
var secureString by persistManager.preference("default")

secureString = "newSecureValue"
val storedValue = secureString

DataStore Encryption

Unlike SharedPreferences, DataStore does not natively support encryption. SecurePersist provides the missing functionality to securely handle DataStore preferences, ensuring your data is encrypted with the same zero-configuration approach.

DataStore Example

SecurePersist extends encryption to DataStore and provides encrypted and unencrypted access with zero-configuration.

// Save a non-encrypted preference
persistManager.putDataStorePreference("key2", 123)

// Retrieve a non-encrypted preference
val number: Int = persistManager.getDataStorePreference("key2", 0)

// Encrypt and save a preference
persistManager.encryptDataStorePreference("key3", true)

// Decrypt and retrieve a preference
val flag: Boolean = persistManager.decryptDataStorePreference("key3", false)

// Delete a preference
persistManager.deleteDataStorePreference("key2")

EncryptionManager

EncryptionManager provides additional functionality for encrypting and decrypting raw data.

It allows you to save your encryption key and pass it to a server, and thus also allows to pass during construction or with a setter such a key to use. If you don’t pass an external key, the library will create a custom key and push it to the KeyStore so it can be used as long as you don’t uninstall your app.

Currently, the EncryptionManager will encrypt and decrypt the following types:

  • Boolean
  • Int
  • Float
  • Long
  • String

for any other type it will throw an IllegalArgumentException("Unsupported type") exception.

So the EncryptionManager currently accepts the types that are also accepted on SharedPreferences and DataStore.

Initialization

You can initialize using the KeyStore

val encryptionManager = EncryptionManager.withKeyStore("your_key_alias")

You can initialize using the External Key

First, generate an external key:

val externalKey = EncryptionManager.generateExternalKey()

Then, use the generated key to create an instance of EncryptionManager:

val encryptionManager = EncryptionManager.withExternalKey(externalKey)

Chaining Initialization

You can also initialize using a method chaining approach. This allows you to configure the EncryptionManager with both a key from the Android KeyStore and an external key if needed.

val encryptionManager = EncryptionManager
.withKeyStore("myKeyAlias")
.withExternalKey(EncryptionManager.generateExternalKey())

Encryption Details

  • Algorithm: AES (Advanced Encryption Standard)
  • Mode: GCM (Galois/Counter Mode)
  • Padding: No Padding (GCM handles it internally)
  • Key Management: Managed by Android KeyStore
  • Key Strength: 256-bit keys for strong encryption

AES in GCM mode is an authenticated encryption algorithm that provides both data confidentiality and integrity. This makes it highly secure and suitable for sensitive data. Using the Android KeyStore for key management adds an extra layer of security by storing keys in a secure, hardware-backed environment.

Encrypting and Decrypting Raw Data

val encryptionManager = EncryptionManager("your_key_alias")

// Encrypt data
val encryptedData = encryptionManager.encryptData("plainText")

// Decrypt data
val decryptedData = encryptionManager.decryptData(encryptedData)
val plainText = String(decryptedData, Charsets.UTF_8)

// Encrypt a value and encode it to a Base64 string
val encryptedValue = encryptionManager.encryptValue("valueToEncrypt")

// Decrypt a Base64 encoded string and return the original value
val decryptedValue = encryptionManager.decryptValue(encryptedValue, "defaultValue")

Encrypting and Decrypting Raw Data with an External Key

One important feature is the ability generate an external key, which you can then pass to the library.

By doing so you can safe-keep that key at some server in order to be able to make use of it when needed in the future.

// Generate an external key
val externalKey = EncryptionManager.generateExternalKey()

// Create an EncryptionManager instance with the external key
val encryptionManager = EncryptionManager
.withKeyStore("myKeyAlias")
.withExternalKey(externalKey)

Also, you can supply that key at runtime

// Generate an external key
val externalKey = EncryptionManager.generateExternalKey()

// Create an EncryptionManager instance
val encryptionManager = EncryptionManager
.withKeyStore("myKeyAlias")

// now that will replace the default key
encryptionManager.setExternalKey(externalKey)

You can supply an external also only for a specific entryption/decryption in Static context, leaving the default key for everything else

// Create an EncryptionManager instance
val encryptionManager = EncryptionManager
.withKeyStore("myKeyAlias")

// Generate an external key
val externalKey = EncryptionManager.generateExternalKey()

// we will now use that key only for the specified encryptions/decryptions
// Encrypt a value and encode it to a Base64 string with custom key
val encryptedValue1 = EncryptionManager.encryptValue("valueToEncrypt", secretKey = externalKey)

// Encrypt a value and encode it to a Base64 string with default key
val encryptedValue2 = encryptionManager.encryptValue("valueToEncrypt")

// Decrypt a Base64 encoded string and return the original value with custom key
val decryptedValue1 = EncryptionManager.decryptValue(encryptedValue, "defaultValue", secretKey = externalKey)

// Decrypt a Base64 encoded string and return the original value with default key
val decryptedValue2 = encryptionManager.decryptValue(encryptedValue, "defaultValue")

Storing the externalKey (SecretKey) to some other medium

If you have generated a key and want to securely transmit it to some API or retrieve it, then this library provides two convenience static methods for encoding and decoding that key to a string so you can easily transfer it.

// generate a key
val originalKey = EncryptionManager.generateExternalKey()

// encrypt your data with that external key
val encryptedData = EncryptionManager.encryptData("Hello, Secure World!", originalKey)

// create a string that contains the encoded key (maybe then send it to some server)
val encodedKey: String = EncryptionManager.encodeSecretKey(originalKey)

// create another key from that encoded string (maybe you got that as a string from a server)
val decodedKey: SecretKey = EncryptionManager.decodeSecretKey(encodedKey)

// as you can see, you can decode the encrypted data using the key that was reconstructed from the encoded string
val decryptedText = EncryptionManager.decryptData(encryptedData, decodedKey)

Testing

You can find extended tests inside the androidTest folder for both the PersistManager and the Encryption manager to have even more examples of their usage.

Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

License

This project is licensed under the MIT License

GitHub Repository

You can find this library at GitHub

--

--

Ioannis Anifantakis

MSc Computer Science. — Software engineer and programming instructor. Actively involved in Android Development and Deep Learning.