The galaxy has the Guardians of the Galaxy to fend off galactic threats and save us from cosmic chaos. But who’s watching over your apps, shielding them from malicious attackers and software mischief?
Enter Ackee Guardian – your app's fearless defender, standing strong against the dark forces of bugs, vulnerabilities, and threats. Just as the Guardians of the Galaxy protect the universe, Ackee Guardian ensures your Android app stays secure and your users stay safe.
Ackee Guardian focuses on cryptography and in Ackee we use it to share some common cryptographic implementations across our projects, but it contains useful logic suited for anyone's needs. More specifically, you can use it as a 100% compatible replacement for Jetpack Security Crypto library and there is more!
Library consists of several modules:
core
contains some basic core logic likeMasterKey
class that is being used by other modules of the librarydatastore
provides encryptedDataStore
implementationdatastore-preferences
provides encryptedPreferenceDataStore
implementationjetpack
is a rewritten and improved Jetpack Security library
Contains basic core cryptographic logic like MasterKey
class (rewritten from Jetpack Security)
that is used by other modules to encrypt the data. You don't have to depend on this module directly,
if you use datastore
modules or jetpack
.
DataStore modules provide an encrypted version of DataStore
s. They use Tink
library for cryptographic algorithms under the hood the same as original Jetpack Security and
jetpack
module as well.
This was the main reason why we decided to create this library. We have been using Jetpack Security library on several projects, but it had some issues. First, it was silently deprecated without providing any alternative. The latest stable version was released in 2021, which is pretty old, especially considering that it is a library focused on security. There were several issues, some of them fixed in alpha releases, but they never made it to stable. Since we have been already using Jetpack Security, needed some alternative and otherwise liked the abstractions it provided, we decided to completely rewrite it and fix the known issues it had.
jetpack
module contains EncryptedSharedPreferences
and EncryptedFile
implementations. MasterKey
is ported as well, but is part of core
module, because it is reused by other modules as well. However,
if you need original Jetpack Security functionality, it is sufficient to depend on jetpack
only and
core
is included automatically.
jetpack
is 100% compatible in terms of data compatibility with Jetpack Security. It means that if
you already use Jetpack Security on your project and want to switch to jetpack
, you can just replace
the library, make necessary adjustments to source code and run the app. The already created encrypted data
will work fine with the jetpack
implementation.
Regarding source code compatibility, we had to make some big necessary breaking changes to improve the implementation and we also did some smaller not necessary breaking changes, which are easy to adapt to, but we believe they improve the API. We tried to keep the API as consistent with Jetpack Security as possible and only broke it when it provided some benefits.
The smaller changes mostly involve MasteKey
class changes. When you use this class to get a master key,
it actually returns its instance that needs to be passed to encrypted implementations. This provides
a more type-safe API compared to a general String
representation.
The biggest breaking changes involve EncryptedSharedPreferences
. The original Jetpack Security's
implementation returned the instance of SharedPreferences
, which was beneficial, because you could
have used this on all places where you needed a regular SharedPreferences
types. However, there were
also some problems. Those problems might not be noticeable for a few key-value pairs stored to preferences,
but becomes visible for a lot of key-value pairs or data of a bigger size. All crypto operations of
original EncryptedSharedPreferences
(and EncryptedFile
as well) are executed on the caller's thread,
possibly blocking it for more intensive operations. This is especially problematic for methods, where
you do not expect this even for the regular SharedPreferences
like apply
, which is actually one of
those most problematic methods. Since we wanted to improve this and use coroutines for that, we had
to break this completely and we introduced a new EncryptedSharedPreferences
interface that is basically
a 1:1 copy of the SharedPreferences
interface, but have all relevant methods suspend
to not block
caller's thread. We understand, that this big breaking change might be problematic for apps relying
heavily on SharedPreferences
(e.g. passing it to a third-party library), so there is also an extension
EncryptedSharedPreferences.adaptToSharedPreferences
, which adapts EncryptedSharedPreferences
to
SharedPreferences
. However, you should not use this, unless really necessary, and you should migrate
to EncryptedSharedPreferences
to get all benefits it offers, as the adapter just blocks while waiting
for the internal EncryptedSharedPreferences
suspend functions to complete.
During rewrite of Jetpack Security library we made following improvements:
- Rewritten from Java to 100% Kotlin.
- All logic is covered by tests. We followed a careful process of refactoring, when we first covered all the existing functionality by tests and then started to rewrite the implementations, which gave us a confidence to not break anything.
- Improve some APIs like
MasterKey
, which is now more type-safe and also offersKeyGenParameterSpec.Builder
configured with the same default values as the original implementation, but you can take this and apply additional custom configurations before building the final spec and getting a key. - Improve performance of the
EncryptedSharedPreferences
for various methods like getting all key-value pairs that made unnecessary extra encryptions/decryptions under the hood. - Remove all blocking calls and making heavy methods suspend instead.
- Fix synchronization issues during master key creation and increase Tink library version from the old one, used in Jetpack Security, that also had some synchronization issues, that were fixed in later releases.
- Since one of the major issues of Jetpack Security was an outdated Tink library, which makes all the crypto operations and Jetpack Security was basically just a thin abstraction over it, we wanted to try to prevent the same issues in the future and so we decided to force clients of Ackee Guardian library to depend on Tink explicitly. This allows clients to have a better control over updates, independent of Ackee Guardian updates.
- Fix several bugs discovered in
EncryptedSharedPreferences
during covering the logic by tests:- If you saved empty string
Set
, you didn't get it back by usinggetStringSet
, but you got default value passed in parameter instead. - Storing
Set
with null threw NPE. get*
methods didn't throwClassCastException
as specified inSharedPreferences
contracts, when you tried to access some key using an incorrect get method.- Contract of
SharedPreferences.registerOnSharedPreferenceChangeListener
specifies that it does not store strong references on the listener objects, but it actually incorrectly did. - Contract of
OnSharedPreferenceChangeListener.onSharedPreferenceChanged
specifies, that it has to be invoked from the main thread, but this was not ensured. OnSharedPreferenceChangeListener.onSharedPreferenceChanged
was being called multiple times per one key in one editor, if the editor did multiple changes on the same key.OnSharedPreferenceChangeListener.onSharedPreferenceChanged
was being called even when the key was added and then removed in the same editor.
- If you saved empty string
Add the following dependencies to your libs.versions.toml
, depending on what you need. You should
always use BOM to be sure to get binary compatible dependencies. If you need only jetpack
features,
just declare BOM and io.github.ackeecz:guardian-jetpack
. If you need only particular DataStore,
then declare BOM and particular DataStore dependency, e.g. io.github.ackeecz:guardian-datastore
.
You don't need to declare io.github.ackeecz:guardian-core
dependency, unless you depend only on
core
without any DataStore or jetpack
modules.
[versions]
ackee-guardian-bom = "SPECIFY_VERSION"
tink = "SPECIFY_VERSION"
[libraries]
ackee-guardian-bom = { module = "io.github.ackeecz:guardian-bom", version.ref = "ackee-guardian-bom" }
ackee-guardian-core = { module = "io.github.ackeecz:guardian-core" }
ackee-guardian-datastore = { module = "io.github.ackeecz:guardian-datastore" }
ackee-guardian-datastore-preferences = { module = "io.github.ackeecz:guardian-datastore-preferences" }
ackee-guardian-jetpack = { module = "io.github.ackeecz:guardian-jetpack" }
tink-android = { module = "com.google.crypto.tink:tink-android", version.ref = "tink" }
Then specify dependencies in your build.gradle.kts
:
dependencies {
// Always use BOM
implementation(platform(libs.ackee.guardian.bom))
// Optional core dependency. Needed to be specified only if you do not use any other artifact
// and want to use core in your app.
implementation(libs.ackee.guardian.core)
// For encrypted DataStore
implementation(libs.ackee.guardian.datastore)
// For encrypted preferences DataStore
implementation(libs.ackee.guardian.datastore.preferences)
// For Jetpack Security port
implementation(libs.ackee.guardian.jetpack)
// Dependency on Tink must be included explicitly. This allows clients of Ackee Guardian library
// to control the version of Tink themselves, being able to keep it up-to-date as much as possible
// and not depend on Ackee Guardian releases.
implementation(libs.tink.android)
}
Basic usage of the main library functionality is described bellow. You can also take a look on tests to get even more detailed picture.
The usages of encrypted DataStore implementations are almost the same as the classic DataStore. Both
classic and preferences encrypted DataStore implementations can be created using property delegates
or factories. The main difference is the DataStoreCryptoParams
class that contains necessary
parameters specific to crypto operations over DataStore. Check the documentation of this class for
more details of what you can customize. Once you create an encrypted version of DataStore, you can
use it exactly the same as the classic unencrypted DataStore instance.
Encrypted DataStore delegate:
val Context.myDataStore by encryptedDataStore(
cryptoParams = DataStoreCryptoParams(
encryptionScheme = DataStoreEncryptionScheme.AES256_GCM_HKDF_4KB,
getMasterKey = { MasterKey.getOrCreate() },
),
fileName = "filename",
serializer = serializer,
// Other params as in dataStore delegate
)
Encrypted DataStore factory:
DataStoreFactory.createEncrypted(
context = context,
cryptoParams = DataStoreCryptoParams(
encryptionScheme = DataStoreEncryptionScheme.AES256_GCM_HKDF_4KB,
getMasterKey = { MasterKey.getOrCreate() },
),
serializer = serializer,
produceFile = { context.dataStoreFile("encrypted_data") },
// Other params as in DataStoreFactory.create
)
Encrypted PreferenceDataStore delegate:
val Context.myDataStore by encryptedPreferencesDataStore(
cryptoParams = DataStoreCryptoParams(
encryptionScheme = DataStoreEncryptionScheme.AES256_GCM_HKDF_4KB,
getMasterKey = { MasterKey.getOrCreate() },
),
name = "preferences_name",
// Other params as in preferencesDataStore delegate
)
Encrypted PreferenceDataStore factory:
PreferenceDataStoreFactory.createEncrypted(
context = context,
cryptoParams = DataStoreCryptoParams(
encryptionScheme = DataStoreEncryptionScheme.AES256_GCM_HKDF_4KB,
getMasterKey = { MasterKey.getOrCreate() },
),
produceFile = { context.preferencesDataStoreFile("encrypted_data") },
// Other params as in PreferenceDataStoreFactory.create
)
Using classes from jetpack
module is almost the same as using the Jetpack Security classes.
EncryptedFile
:
val encryptedFile = EncryptedFile.Builder(
file = File(context.filesDir, "secret_data"),
context = context,
encryptionScheme = EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB,
getMasterKey = { MasterKey.getOrCreate() },
).build()
// Write to the encrypted file
val encryptedOutputStream = encryptedFile.openFileOutput()
// Read the encrypted file
val encryptedInputStream = encryptedFile.openFileInput()
EncryptedSharedPreferences
:
val encryptedSharedPreferences: EncryptedSharedPreferences = EncryptedSharedPreferences.create(
fileName = "secret_shared_prefs",
getMasterKey = { MasterKey.getOrCreate() },
context = context,
prefKeyEncryptionScheme = EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
prefValueEncryptionScheme = EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
// Use EncryptedSharedPreferences and Editor as you would normally use SharedPreferences
encryptedSharedPreferences.edit {
putString("secret_key", "secret_value")
}
As discussed above in Architecture section, EncryptedSharedPreferences.create
no longer return
SharedPreferences
type but a new EncryptedSharedPreferences
. You are highly encouraged to
use this new type, but if you really do need SharedPreferences
, there is an extension, that
can adapt EncryptedSharedPreferences
to SharedPreferences
.
val sharedPreferences: SharedPreferences = encryptedSharedPreferences.adaptToSharedPreferences()
Developed by Ackee team with 💙.
MasterKey
class from core
and EncryptedFile
and EncryptedSharedPreferences
classes from
jetpack
are based on Jetpack Security Crypto library
published by Google LLC, under the Apache License 2.0.