Skip to content

Commit 076b64c

Browse files
committed
Initial encrypted datastore
This is very much a first draft as we need to workout how this and the core module work together better.
1 parent 3972694 commit 076b64c

12 files changed

Lines changed: 369 additions & 4 deletions

File tree

armadillo-datastore/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

armadillo-datastore/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
TODO Add some docs for
2+
3+
4+
## Protobuf
5+
### Kotlinx TODO
6+
7+
### [Wire](https://github.com/square/wire) TODO
8+
9+
```kotlin
10+
plugins {
11+
// TODO add https://github.com/square/wire stuff
12+
}
13+
```
14+
15+
### Google protobuf
16+
17+
If you are new to Google Protobuf library it can be hard to setup. If
18+
you want a quickstart config this should get everything you need going.
19+
20+
```kotlin
21+
import com.google.protobuf.gradle.builtins
22+
import com.google.protobuf.gradle.generateProtoTasks
23+
import com.google.protobuf.gradle.protoc
24+
25+
plugins {
26+
id("com.android.library")
27+
// everything else…
28+
id("com.google.protobuf") version "0.8.13"
29+
}
30+
31+
32+
33+
protobuf {
34+
protobuf.protoc {
35+
artifact = "com.google.protobuf:protoc:3.13.0"
36+
}
37+
protobuf.generateProtoTasks {
38+
all().forEach { task ->
39+
task.builtins {
40+
create("java").option("lite")
41+
}
42+
}
43+
}
44+
}
45+
46+
dependencies {
47+
implementation("com.google.protobuf:protobuf-javalite:3.13.0")
48+
}
49+
```
50+
51+
Sample proto class.
52+
```proto
53+
syntax = "proto3";
54+
55+
option java_package = "at.favre.lib.armadillo.datastore";
56+
option java_outer_classname = "EncryptedPreferencesProto";
57+
58+
message User {
59+
string name = 1;
60+
string email = 2;
61+
}
62+
```
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
plugins {
2+
id("com.android.library")
3+
id("kotlin-android")
4+
id ("org.jetbrains.kotlin.plugin.serialization") version "1.4.10"
5+
}
6+
7+
android {
8+
val compileSdkVersion: Int by rootProject.extra
9+
val buildToolsVersion: String by rootProject.extra
10+
11+
compileSdkVersion(compileSdkVersion)
12+
buildToolsVersion(buildToolsVersion)
13+
14+
defaultConfig {
15+
val minSdkVersion: Int by rootProject.extra
16+
val targetSdkVersion: Int by rootProject.extra
17+
18+
minSdkVersion(minSdkVersion)
19+
targetSdkVersion(targetSdkVersion)
20+
versionCode = 1
21+
versionName = "0.1.0"
22+
23+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
24+
consumerProguardFiles("consumer-rules.pro")
25+
}
26+
27+
buildTypes {
28+
getByName("release") {
29+
isMinifyEnabled = false
30+
proguardFiles(
31+
getDefaultProguardFile("proguard-android-optimize.txt"),
32+
"proguard-rules.pro"
33+
)
34+
}
35+
}
36+
37+
compileOptions {
38+
sourceCompatibility = JavaVersion.VERSION_1_8
39+
targetCompatibility = JavaVersion.VERSION_1_8
40+
}
41+
42+
kotlinOptions {
43+
jvmTarget = JavaVersion.VERSION_1_8.toString()
44+
}
45+
}
46+
47+
dependencies {
48+
implementation(project(":armadillo"))
49+
50+
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.4.10")
51+
52+
implementation("androidx.core:core-ktx:1.3.2")
53+
implementation("androidx.appcompat:appcompat:1.2.0")
54+
implementation("androidx.datastore:datastore-core:1.0.0-alpha01")
55+
56+
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.0.0")
57+
58+
testImplementation("junit:junit:4.13")
59+
androidTestImplementation("androidx.test.ext:junit:1.1.2")
60+
androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
61+
androidTestImplementation("org.bouncycastle:bcprov-jdk15on:1.60")
62+
androidTestImplementation("org.mindrot:jbcrypt:0.4")
63+
androidTestImplementation("androidx.test.ext:junit:1.1.2")
64+
androidTestImplementation("androidx.test:rules:1.3.0")
65+
}

armadillo-datastore/consumer-rules.pro

Whitespace-only changes.

armadillo-datastore/proguard-rules.pro

Whitespace-only changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package at.favre.lib.armadillo.datastore
2+
3+
import kotlinx.serialization.Serializable
4+
5+
@Serializable
6+
data class User(
7+
val name: String,
8+
val email: String,
9+
)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package at.favre.lib.armadillo.datastore
2+
3+
import android.content.Context
4+
import androidx.datastore.DataStore
5+
import androidx.datastore.createDataStore
6+
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.serialization.ExperimentalSerializationApi
8+
import kotlinx.serialization.protobuf.ProtoBuf
9+
10+
@ExperimentalSerializationApi
11+
class UserStore(context: Context) {
12+
private val serializer = User.serializer()
13+
private val protobuf = ProtoBuf {}
14+
15+
private val protocol = object : ProtobufProtocol<User> {
16+
17+
override fun fromBytes(bytes: ByteArray): User =
18+
protobuf.decodeFromByteArray(serializer, bytes)
19+
20+
override fun fromNothing(): User =
21+
User(name = "", email = "")
22+
23+
override fun toBytes(data: User): ByteArray =
24+
protobuf.encodeToByteArray(serializer, data)
25+
}
26+
27+
private val userSerializer: ArmadilloSerializer<User> =
28+
ArmadilloSerializer(
29+
context = context,
30+
protocol = protocol
31+
)
32+
33+
private val store: DataStore<User> = context.createDataStore(
34+
fileName = "user.pb",
35+
serializer = userSerializer
36+
)
37+
38+
suspend fun update(reduce: (User) -> User) {
39+
store.updateData { user -> reduce(user) }
40+
}
41+
42+
val user: Flow<User>
43+
get() = store.data
44+
45+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package at.favre.lib.armadillo.datastore
2+
3+
import androidx.test.core.app.ApplicationProvider
4+
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
import kotlinx.coroutines.flow.first
6+
import kotlinx.coroutines.runBlocking
7+
import kotlinx.serialization.ExperimentalSerializationApi
8+
import org.junit.Test
9+
import org.junit.runner.RunWith
10+
11+
@ExperimentalSerializationApi
12+
@RunWith(AndroidJUnit4::class)
13+
class UserStoreTest {
14+
15+
@Test
16+
fun canReadEmptyUserFromStore() {
17+
val store = UserStore(ApplicationProvider.getApplicationContext())
18+
19+
val user = runBlocking { store.user.first() }
20+
21+
assert(user.name == "")
22+
assert(user.email == "")
23+
}
24+
25+
26+
@Test
27+
fun canUserStore_andReadFromStore() {
28+
val store = UserStore(ApplicationProvider.getApplicationContext())
29+
val newName = "new name"
30+
31+
runBlocking {
32+
store.update {
33+
it.copy(name = newName)
34+
}
35+
}
36+
val user = runBlocking { store.user.first() }
37+
38+
assert(user.name == newName)
39+
assert(user.email == "")
40+
}
41+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest package="at.favre.lib.armadillo.datastore" />
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package at.favre.lib.armadillo.datastore
2+
3+
import android.content.Context
4+
import android.os.Build
5+
import android.util.Log
6+
import androidx.datastore.Serializer
7+
import at.favre.lib.armadillo.*
8+
import at.favre.lib.armadillo.Armadillo.CONTENT_KEY_OUT_BYTE_LENGTH
9+
import at.favre.lib.armadillo.BuildConfig
10+
import java.io.InputStream
11+
import java.io.OutputStream
12+
import java.security.Provider
13+
import java.security.SecureRandom
14+
15+
interface ProtobufProtocol<T> {
16+
fun toBytes(data: T): ByteArray
17+
fun fromBytes(bytes: ByteArray): T
18+
fun fromNothing(): T
19+
}
20+
21+
class ArmadilloSerializer<T>(
22+
context: Context,
23+
private val protocol: ProtobufProtocol<T>,
24+
fingerprintData: List<String> = emptyList(),
25+
secureRandom: SecureRandom = SecureRandom(),
26+
additionalDecryptionConfigs: List<EncryptionProtocolConfig> = listOf(),
27+
enabledKitkatSupport: Boolean = false,
28+
provider: Provider? = null,
29+
preferencesSalt: ByteArray = BuildConfig.PREF_SALT
30+
) : Serializer<T> {
31+
32+
private val password: ByteArrayRuntimeObfuscator?
33+
private val encryptionProtocol: EncryptionProtocol
34+
private val fingerprint: EncryptionFingerprint = EncryptionFingerprintFactory.create(
35+
context,
36+
buildString { fingerprintData.forEach(::append) }
37+
)
38+
39+
init {
40+
val defaultConfig = EncryptionProtocolConfig.newDefaultConfig()
41+
42+
val stringMessageDigest = HkdfMessageDigest(
43+
BuildConfig.PREF_SALT,
44+
CONTENT_KEY_OUT_BYTE_LENGTH
45+
)
46+
val kitKatConfig = takeIf { enabledKitkatSupport }?.run {
47+
@Suppress("DEPRECATION")
48+
EncryptionProtocolConfig.newBuilder(defaultConfig.build())
49+
.authenticatedEncryption(AesCbcEncryption(secureRandom, provider))
50+
.protocolVersion(Armadillo.KITKAT_PROTOCOL_VERSION)
51+
.build()
52+
}
53+
val config =
54+
if (kitKatConfig != null && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
55+
kitKatConfig
56+
} else {
57+
EncryptionProtocolConfig
58+
.newBuilder(defaultConfig.build())
59+
.authenticatedEncryption(AesGcmEncryption(secureRandom, provider))
60+
.build()
61+
}.also { checkKitKatSupport(it.authenticatedEncryption) }
62+
63+
val factory = DefaultEncryptionProtocol.Factory(
64+
config,
65+
fingerprint,
66+
stringMessageDigest,
67+
secureRandom,
68+
false, // enableDerivedPasswordCache,
69+
if (enabledKitkatSupport) {
70+
additionalDecryptionConfigs + kitKatConfig
71+
} else {
72+
additionalDecryptionConfigs
73+
},
74+
)
75+
76+
encryptionProtocol = factory.create(preferencesSalt)
77+
password = null // TODO Add password config factory.obfuscatePassword()
78+
}
79+
80+
81+
private fun checkKitKatSupport(authenticatedEncryption: AuthenticatedEncryption) {
82+
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT && authenticatedEncryption.javaClass == AesGcmEncryption::class.java) {
83+
throw UnsupportedOperationException("aes gcm is not supported with KitKat, add support " +
84+
"manually with Armadillo.Builder.enableKitKatSupport()")
85+
}
86+
}
87+
88+
companion object {
89+
private const val CRYPTO_KEY = "ArmadilloStore"
90+
}
91+
92+
93+
private fun encrypt(content: ByteArray): ByteArray =
94+
try {
95+
encryptionProtocol
96+
.encrypt(
97+
encryptionProtocol.deriveContentKey(CRYPTO_KEY),
98+
encryptionProtocol.deobfuscatePassword(password),
99+
content
100+
)
101+
} catch (e: Throwable) {
102+
throw IllegalStateException(e)
103+
}
104+
105+
106+
private fun decrypt(encrypted: ByteArray): ByteArray? {
107+
if (encrypted.isEmpty()) {
108+
return null
109+
}
110+
try {
111+
return encryptionProtocol
112+
.decrypt(
113+
encryptionProtocol.deriveContentKey(CRYPTO_KEY),
114+
encryptionProtocol.deobfuscatePassword(password),
115+
encrypted
116+
)
117+
} catch (e: Throwable) {
118+
Log.e("DataStrore", "decetyp", e)
119+
// recoveryPolicy.handleBrokenConte(e, keyHash, base64Encrypted, password != null, this)
120+
// TODO handle this
121+
}
122+
return null
123+
}
124+
125+
override fun readFrom(input: InputStream): T =
126+
input
127+
.readBytes()
128+
.let(::decrypt)
129+
.let {
130+
val bytes = it ?: byteArrayOf()
131+
if (bytes.isEmpty()) protocol.fromNothing()
132+
else protocol.fromBytes(bytes)
133+
}
134+
135+
136+
override fun writeTo(t: T, output: OutputStream) {
137+
protocol
138+
.toBytes(t)
139+
.let(::encrypt)
140+
.also(output::write)
141+
}
142+
}

0 commit comments

Comments
 (0)