Skip to content

Commit

Permalink
Merge pull request #8 from osipxd/security-crypto
Browse files Browse the repository at this point in the history
New library `security-crypto-datastore`
  • Loading branch information
osipxd authored Nov 18, 2022
2 parents b5f6d62 + 9d0f7af commit b7b4b67
Show file tree
Hide file tree
Showing 21 changed files with 446 additions and 61 deletions.
70 changes: 44 additions & 26 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,42 @@
## [Unreleased]

:warning: **Breaking change:** `PreferenceDataStoreFactory.createEncrypted` extension has been moved to separated module. To continue use it, change the dependency module in your build script:
## [1.0.0-alpha03] - 2022.11.18

```diff
-implmentation("io.github.osipxd:encrypted-datastore:...")
+implmentation("io.github.osipxd:encrypted-datastore-preferences:...")
```
#### More high-level library `security-crypto-datastore`

#### Streaming serializer
New library provides more simple and less error-prone API to create encrypted DataStores.
All Tink-related stuff hidden from you in `security-crypto` library, and all you should do is wrap `File` with `EncryptedFile`:

Introduced new extension-function `Serializer.encrypted(StreamingAead)` to encrypt DataStore in streaming manneer.
Old extension-function with `Aead` is not planned to be removed yet, but for all new code it is recommended to use the new function.
You can obtain `StreamingAead` similar to `Aead`:
```kotlin
val dataStore = DataStoreFactory.createEncrypted(serializer) {
EncryptedFile.Builder(
context.dataStoreFile("filename"),
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}
```

Or even simpler, if you use `security-crypto-ktx:1.1.0`:

```kotlin
// Remember to initialize Tink
//AeadConfig.register()
StreamingAeadConfig.register()

val handle = AndroidKeysetManager.Builder()
.withSharedPref(context, "master_keyset", "master_key_preference")
// Change key template AES256_GCM -> AES256_GCM_HKDF_4KB
//.withKeyTemplate(KeyTemplates.get("AES256_GCM"))
.withKeyTemplate(KeyTemplates.get("AES256_GCM_HKDF_4KB"))
.withMasterKeyUri("android-keystore://master_key")
.build()
.keysetHandle

// Get StreamingAead instead of Aead
//val aead = handle.getPrimitive(Aead::class.java)
val streamingAead = handle.getPrimitive(StreamingAead::class.java)
val dataStore = DataStoreFactory.createEncrypted(serializer) {
EncryptedFile(
context = context,
file = context.dataStoreFile("filename"),
masterKey = MasterKey(context)
)
}
```

See the [Migration guide](README.md#migration).

#### Streaming serializer

Introduced new extension-function `Serializer.encrypted(StreamingAead)` to encrypt DataStore in streaming manner.
Old extension-function with `Aead` is not planned to be removed yet, but for all new code it is recommended to use the new function or migrate to the `security-crypto-datastore`.

> **ATTENTION!**
> You can not use `StreamingAead` to decrypt data encrypted with `Aead`,
> so you can not just replace `Aead` with `StreamingAead` without migration.
Expand All @@ -40,6 +45,18 @@ val streamingAead = handle.getPrimitive(StreamingAead::class.java)
> 2. **Do nothing** - continue to use `Aead`
> 3. **Destructive migration** - specify `CorruptionHandler` to replace old content with something else
#### New module `encrypted-datastore-preferences`

:warning: **Breaking change:**

All stuff related to Preference DataStore was moved to `io.github.osipxd:encrypted-datastore-preferences`.
To continue use it, change the dependency module in your build script:

```diff
-implmentation("io.github.osipxd:encrypted-datastore:...")
+implmentation("io.github.osipxd:encrypted-datastore-preferences:...")
```

### Fixed

- Fixed crash when DataStore can not be decrypted (#1)
Expand All @@ -55,4 +72,5 @@ val streamingAead = handle.getPrimitive(StreamingAead::class.java)
- gradle-infrastructure `0.12.1``0.17`
- Migrate dependencies to version catalogs

[unreleased]: https://github.com/osipxd/encrypted-datastore/compare/v1.0.0-alpha02...main
[unreleased]: https://github.com/osipxd/encrypted-datastore/compare/v1.0.0-alpha03...main
[v1.0.0-alpha03]: https://github.com/osipxd/encrypted-datastore/compare/v1.0.0-alpha02...v1.0.0-alpha03
129 changes: 115 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# encrypted-datastore
# Encrypted DataStore
[![Version](https://img.shields.io/maven-central/v/io.github.osipxd/encrypted-datastore?style=flat-square)][mavenCentral] [![License](https://img.shields.io/github/license/osipxd/encrypted-datastore?style=flat-square)][license]

Extensions to encrypt DataStore using [Tink].
Extensions to store DataStore in `EncryptedFile`.

> :warning: This tiny library will be maintained until an official solution for DataStore encryption will be released by Google.
> :warning: This tiny library will be maintained until an official solution for DataStore encryption will be released by Google. \
> Vote for this feature on issue tracker: [b/167697691](https://issuetracker.google.com/issues/167697691)
---

Expand All @@ -18,15 +19,93 @@ repositories {
}

dependencies {
implementation("io.github.osipxd:encrypted-datastore:1.0.0-alpha02")
implementation("io.github.osipxd:security-crypto-datastore:1.0.0-alpha03")
// Or, if you want to use Preferences DataStore:
implementation("io.github.osipxd:security-crypto-datastore-preferences:1.0.0-alpha03")
}
```

> **Dependencies:**
> - `security-crypto` [1.0.0](https://developer.android.com/jetpack/androidx/releases/security#1.0.0)
> - `datastore` [1.0.0](https://developer.android.com/jetpack/androidx/releases/datastore#1.0.0)
> - `tink` [1.7.0](https://github.com/google/tink/releases/tag/v1.7.0)
## Usage

First, you need to create `Aead` object to encrypt DataStore or you may use already created one:
To create encrypted DataStore, just use method `DataStoreFactory.createEncryptred` instead of `create` and
provide `EncryptedFile` instead of `File`:

```kotlin
val dataStore = DataStoreFactory.createEncrypted(serializer) {
EncryptedFile.Builder(
context.dataStoreFile("filename"),
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}
```

<details>
<summary>Or even simpler, if you use <code>security-crypto-ktx:1.1.0</code></summary>

```kotlin
val dataStore = DataStoreFactory.createEncrypted(serializer) {
EncryptedFile(
context = context,
file = context.dataStoreFile("filename"),
masterKey = MasterKey(context)
)
}
```
</details>

Similarly, you can create Preferences DataStore:

```kotlin
val dataStore = DataStoreFactory.createEncrypted(serializer) {
EncryptedFile.Builder(
// The file should have extension .preferences_pb
context.dataStoreFile("filename.preferences_pb"),
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}
```

<details>
<summary>Or even simpler, if you use <code>security-crypto-ktx:1.1.0</code></summary>

```kotlin
val dataStore = PreferenceDataStoreFactory.createEncrypted {
EncryptedFile(
context = context,
// The file should have extension .preferences_pb
file = context.dataStoreFile("filename.preferences_pb"),
masterKey = MasterKey(context)
)
}
```
</details>

## Migration

### Migrate from `encrypted-datastore` to `security-crypto-datastore`

Change the dependency in build script:

```diff
dependencies {
- implementation("io.github.osipxd:encrypted-datastore:...")
+ implementation("io.github.osipxd:security-crypto-datastore:...")
}
```

New library uses `StreamingAead` instead of `Aead` under the hood, so to not lose the previously encrypted data you should specify `fallbackAead`:

```kotlin
// This AEAD was used to encrypt DataStore previously, we will use it as fallback
val aead = AndroidKeysetManager.Builder()
.withSharedPref(context, "master_keyset", "master_key_preference")
.withKeyTemplate(KeyTemplates.get("AES256_GCM"))
Expand All @@ -36,25 +115,47 @@ val aead = AndroidKeysetManager.Builder()
.getPrimitive(Aead::class.java)
```

Then you can make any DataStore Serializer encrypted using extension-function `Serializer<T>.encrypted(Aead)`:

The old code to create DataStore was looking like this:
```kotlin
object ProtoProfileSerializer : Serializer<Profile> {
// serializer implementation here
val dataStore = DataStoreFactory.create(serializer.encrypted(aead)) {
context.dataStoreFile("filename")
}
```

val dataStore = DataStoreFactory.create(ProtoProfileSerializer.encrypted(aead)) {
context.dataStoreFile("proto_profile")
The new code will look like this:
```kotlin
val dataStore = DataStoreFactory.createEncrypted(
serializer,
encryptionOptions = {
// Specify fallback Aead to make it possible to decrypt data encrypted with it
fallbackAead = aead
}
) {
EncryptedFile.Builder(
context.dataStoreFile("filename"), // Keep the same file
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}
```

If you need to create encrypted `PreferenceDataStore`, use function `createEncrypted` instead of `create`:
<details>
<summary>Or even simpler, if you use <code>security-crypto-ktx:1.1.0</code></summary>

```kotlin
val prefsDataStore = PreferenceDataStoreFactory.createEncrypted(aead) {
context.preferencesDataStoreFile("user_preferences")
val dataStore = DataStoreFactory.createEncrypted(
serializer,
encryptionOptions = { fallbackAead = aead }
) {
EncryptedFile(
context = context,
file = context.dataStoreFile("filename"), // Keep the same file
masterKey = MasterKey(context)
)
}
```
</details>

### Thanks

Expand Down
15 changes: 12 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import com.redmadrobot.build.dsl.*
import com.redmadrobot.build.dsl.developer
import com.redmadrobot.build.dsl.mit
import com.redmadrobot.build.dsl.setGitHubProject

plugins {
com.redmadrobot.`publish-config`
com.redmadrobot.`android-config`
}

group = "io.github.osipxd"
version = "${libs.versions.datastore.get()}-alpha02"
val datastoreVersion = libs.versions.datastore.get()
subprojects {
group = "io.github.osipxd"
version = "$datastoreVersion-alpha03"
}

redmadrobot {
// Min SDK should be aligned with min SDK in androidx.security:security-crypto
android.minSdk.set(21)

publishing {
signArtifacts.set(true)

Expand Down
3 changes: 3 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ plugins {
}

dependencies {
implementation(libs.infrastructure.android)
implementation(libs.infrastructure.kotlin)
implementation(libs.infrastructure.publish)
}

repositories {
mavenCentral()
google()
gradlePluginPortal()
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import com.redmadrobot.build.dsl.ossrh

plugins {
id("com.redmadrobot.kotlin-library")
id("com.redmadrobot.publish")
}

Expand Down
4 changes: 4 additions & 0 deletions config/lint/lint-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 7.2.1" type="baseline" client="gradle" dependencies="true" name="AGP (7.2.1)" variant="all" version="7.2.1">

</issues>
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ java {
}

dependencies {
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.datastore.preferences.core)
}
5 changes: 3 additions & 2 deletions encrypted-datastore-preferences/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
plugins {
convention.library
com.redmadrobot.`kotlin-library`
convention.publish
}

description = "Extensions to encrypt DataStore Preferences using Tink"

val hackProject = project(":encrypted-datastore-internal-visibility-hack")
dependencies {
api(project(":encrypted-datastore"))
api(libs.androidx.datastore.preferences)
api(libs.androidx.datastore.preferences.core)

// It will be embedded into jar and shouldn't be added to pom.xml file
compileOnly(hackProject)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,9 @@ import java.io.File
/**
* Creates preference DataStore encrypted using the given [aead].
*
* **Deprecated** in favor of version with [StreamingAead].
* You can not use to decrypt data encrypted with `Aead`,
* so you can not just replace `Aead` with `StreamingAead` without migration.
* To not lose your previously encrypted data, you have three options:
* 1. **Migration** - add fallback for `StreamingAead` using function [StreamingAead.withDecryptionFallback]
* 2. **Do nothing** - continue to use this method `Aead`
* 3. **Destructive migration** - specify [ReplaceFileCorruptionHandler] to replace old content with something else
* **Deprecated.**
* It is recommended to migrate to `security-crypto-datastore` library:
* [Migration guide](https://github.com/osipxd/encrypted-datastore#migration)
*/
@Deprecated("Use version of this method with StreamingAead instead of Aead")
@Suppress("UnusedReceiverParameter")
Expand Down
5 changes: 3 additions & 2 deletions encrypted-datastore/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
plugins {
convention.library
com.redmadrobot.`kotlin-library`
convention.publish
}

description = "Extensions to encrypt DataStore using Tink"

dependencies {
api(kotlin("stdlib", version = libs.versions.kotlin.get()))
api(libs.androidx.datastore)
api(libs.androidx.datastore.core)
api(libs.tink)

testImplementation(kotlin("test", version = libs.versions.kotlin.get()))
Expand Down
7 changes: 5 additions & 2 deletions encrypted-datastore/src/main/kotlin/EncryptingSerializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ internal class AeadEncryptingSerializer<T>(
* Adds encryption to [this] serializer using the given [Aead].
*
* **Deprecated** in favor of version with [StreamingAead].
* You can not use to decrypt data encrypted with `Aead`,
* Consider to migrate to `security-crypto-datastore` library:
* [Migration guide](https://github.com/osipxd/encrypted-datastore#migration).
*
* You can not use `StreamingAead` to decrypt data encrypted with `Aead`,
* so you can not just replace `Aead` with `StreamingAead` without migration.
* To not lose your previously encrypted data, you have three options:
* 1. **Migration** - add fallback for `StreamingAead` using function [StreamingAead.withDecryptionFallback]
Expand Down Expand Up @@ -94,7 +97,7 @@ internal class StreamingAeadEncryptingSerializer<T>(
CorruptionException(
"Can not decrypt DataStore using StreamingAead.\n" +
"Probably you should add decryption fallback to Aead:\n" +
"https://github.com/osipxd/encrypted-datastore#migration-to-streamingaead",
"https://github.com/osipxd/encrypted-datastore#migration",
cause = this,
)
} else {
Expand Down
Loading

0 comments on commit b7b4b67

Please sign in to comment.