Skip to content

Commit

Permalink
Initial encrypted datastore
Browse files Browse the repository at this point in the history
This is very much a first draft as we need to workout how this and the core module work together better.
  • Loading branch information
marukami committed Oct 14, 2020
1 parent 6a390ed commit b9c674a
Show file tree
Hide file tree
Showing 12 changed files with 369 additions and 4 deletions.
1 change: 1 addition & 0 deletions armadillo-datastore/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
62 changes: 62 additions & 0 deletions armadillo-datastore/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
TODO Add some docs for


## Protobuf
### Kotlinx TODO

### [Wire](https://github.com/square/wire) TODO

```kotlin
plugins {
// TODO add https://github.com/square/wire stuff
}
```

### Google protobuf

If you are new to Google Protobuf library it can be hard to setup. If
you want a quickstart config this should get everything you need going.

```kotlin
import com.google.protobuf.gradle.builtins
import com.google.protobuf.gradle.generateProtoTasks
import com.google.protobuf.gradle.protoc

plugins {
id("com.android.library")
// everything else…
id("com.google.protobuf") version "0.8.13"
}



protobuf {
protobuf.protoc {
artifact = "com.google.protobuf:protoc:3.13.0"
}
protobuf.generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java").option("lite")
}
}
}
}

dependencies {
implementation("com.google.protobuf:protobuf-javalite:3.13.0")
}
```

Sample proto class.
```proto
syntax = "proto3";
option java_package = "at.favre.lib.armadillo.datastore";
option java_outer_classname = "EncryptedPreferencesProto";
message User {
string name = 1;
string email = 2;
}
```
65 changes: 65 additions & 0 deletions armadillo-datastore/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
plugins {
id("com.android.library")
id("kotlin-android")
id ("org.jetbrains.kotlin.plugin.serialization") version "1.4.10"
}

android {
val compileSdkVersion: Int by rootProject.extra
val buildToolsVersion: String by rootProject.extra

compileSdkVersion(compileSdkVersion)
buildToolsVersion(buildToolsVersion)

defaultConfig {
val minSdkVersion: Int by rootProject.extra
val targetSdkVersion: Int by rootProject.extra

minSdkVersion(minSdkVersion)
targetSdkVersion(targetSdkVersion)
versionCode = 1
versionName = "0.1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}

buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}

dependencies {
implementation(project(":armadillo"))

implementation("org.jetbrains.kotlin:kotlin-stdlib:1.4.10")

implementation("androidx.core:core-ktx:1.3.2")
implementation("androidx.appcompat:appcompat:1.2.0")
implementation("androidx.datastore:datastore-core:1.0.0-alpha01")

androidTestImplementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.0.0")

testImplementation("junit:junit:4.13")
androidTestImplementation("androidx.test.ext:junit:1.1.2")
androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
androidTestImplementation("org.bouncycastle:bcprov-jdk15on:1.60")
androidTestImplementation("org.mindrot:jbcrypt:0.4")
androidTestImplementation("androidx.test.ext:junit:1.1.2")
androidTestImplementation("androidx.test:rules:1.3.0")
}
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package at.favre.lib.armadillo.datastore

import kotlinx.serialization.Serializable

@Serializable
data class User(
val name: String,
val email: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package at.favre.lib.armadillo.datastore

import android.content.Context
import androidx.datastore.DataStore
import androidx.datastore.createDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.protobuf.ProtoBuf

@ExperimentalSerializationApi
class UserStore(context: Context) {
private val serializer = User.serializer()
private val protobuf = ProtoBuf {}

private val protocol = object : ProtobufProtocol<User> {

override fun fromBytes(bytes: ByteArray): User =
protobuf.decodeFromByteArray(serializer, bytes)

override fun fromNothing(): User =
User(name = "", email = "")

override fun toBytes(data: User): ByteArray =
protobuf.encodeToByteArray(serializer, data)
}

private val userSerializer: ArmadilloSerializer<User> =
ArmadilloSerializer(
context = context,
protocol = protocol
)

private val store: DataStore<User> = context.createDataStore(
fileName = "user.pb",
serializer = userSerializer
)

suspend fun update(reduce: (User) -> User) {
store.updateData { user -> reduce(user) }
}

val user: Flow<User>
get() = store.data

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package at.favre.lib.armadillo.datastore

import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
import org.junit.Test
import org.junit.runner.RunWith

@ExperimentalSerializationApi
@RunWith(AndroidJUnit4::class)
class UserStoreTest {

@Test
fun canReadEmptyUserFromStore() {
val store = UserStore(ApplicationProvider.getApplicationContext())

val user = runBlocking { store.user.first() }

assert(user.name == "")
assert(user.email == "")
}


@Test
fun canUserStore_andReadFromStore() {
val store = UserStore(ApplicationProvider.getApplicationContext())
val newName = "new name"

runBlocking {
store.update {
it.copy(name = newName)
}
}
val user = runBlocking { store.user.first() }

assert(user.name == newName)
assert(user.email == "")
}
}
2 changes: 2 additions & 0 deletions armadillo-datastore/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="at.favre.lib.armadillo.datastore" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package at.favre.lib.armadillo.datastore

import android.content.Context
import android.os.Build
import android.util.Log
import androidx.datastore.Serializer
import at.favre.lib.armadillo.*
import at.favre.lib.armadillo.Armadillo.CONTENT_KEY_OUT_BYTE_LENGTH
import at.favre.lib.armadillo.BuildConfig
import java.io.InputStream
import java.io.OutputStream
import java.security.Provider
import java.security.SecureRandom

interface ProtobufProtocol<T> {
fun toBytes(data: T): ByteArray
fun fromBytes(bytes: ByteArray): T
fun fromNothing(): T
}

class ArmadilloSerializer<T>(
context: Context,
private val protocol: ProtobufProtocol<T>,
fingerprintData: List<String> = emptyList(),
secureRandom: SecureRandom = SecureRandom(),
additionalDecryptionConfigs: List<EncryptionProtocolConfig> = listOf(),
enabledKitkatSupport: Boolean = false,
provider: Provider? = null,
preferencesSalt: ByteArray = BuildConfig.PREF_SALT
) : Serializer<T> {

private val password: ByteArrayRuntimeObfuscator?
private val encryptionProtocol: EncryptionProtocol
private val fingerprint: EncryptionFingerprint = EncryptionFingerprintFactory.create(
context,
buildString { fingerprintData.forEach(::append) }
)

init {
val defaultConfig = EncryptionProtocolConfig.newDefaultConfig()

val stringMessageDigest = HkdfMessageDigest(
BuildConfig.PREF_SALT,
CONTENT_KEY_OUT_BYTE_LENGTH
)
val kitKatConfig = takeIf { enabledKitkatSupport }?.run {
@Suppress("DEPRECATION")
EncryptionProtocolConfig.newBuilder(defaultConfig.build())
.authenticatedEncryption(AesCbcEncryption(secureRandom, provider))
.protocolVersion(Armadillo.KITKAT_PROTOCOL_VERSION)
.build()
}
val config =
if (kitKatConfig != null && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
kitKatConfig
} else {
EncryptionProtocolConfig
.newBuilder(defaultConfig.build())
.authenticatedEncryption(AesGcmEncryption(secureRandom, provider))
.build()
}.also { checkKitKatSupport(it.authenticatedEncryption) }

val factory = DefaultEncryptionProtocol.Factory(
config,
fingerprint,
stringMessageDigest,
secureRandom,
false, // enableDerivedPasswordCache,
if (enabledKitkatSupport) {
additionalDecryptionConfigs + kitKatConfig
} else {
additionalDecryptionConfigs
},
)

encryptionProtocol = factory.create(preferencesSalt)
password = null // TODO Add password config factory.obfuscatePassword()
}


private fun checkKitKatSupport(authenticatedEncryption: AuthenticatedEncryption) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT && authenticatedEncryption.javaClass == AesGcmEncryption::class.java) {
throw UnsupportedOperationException("aes gcm is not supported with KitKat, add support " +
"manually with Armadillo.Builder.enableKitKatSupport()")
}
}

companion object {
private const val CRYPTO_KEY = "ArmadilloStore"
}


private fun encrypt(content: ByteArray): ByteArray =
try {
encryptionProtocol
.encrypt(
encryptionProtocol.deriveContentKey(CRYPTO_KEY),
encryptionProtocol.deobfuscatePassword(password),
content
)
} catch (e: Throwable) {
throw IllegalStateException(e)
}


private fun decrypt(encrypted: ByteArray): ByteArray? {
if (encrypted.isEmpty()) {
return null
}
try {
return encryptionProtocol
.decrypt(
encryptionProtocol.deriveContentKey(CRYPTO_KEY),
encryptionProtocol.deobfuscatePassword(password),
encrypted
)
} catch (e: Throwable) {
Log.e("DataStrore", "decetyp", e)
// recoveryPolicy.handleBrokenConte(e, keyHash, base64Encrypted, password != null, this)
// TODO handle this
}
return null
}

override fun readFrom(input: InputStream): T =
input
.readBytes()
.let(::decrypt)
.let {
val bytes = it ?: byteArrayOf()
if (bytes.isEmpty()) protocol.fromNothing()
else protocol.fromBytes(bytes)
}


override fun writeTo(t: T, output: OutputStream) {
protocol
.toBytes(t)
.let(::encrypt)
.also(output::write)
}
}
4 changes: 1 addition & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ buildscript {
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5'
classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.3'
classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.16.0'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10"
}
}

Expand Down
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1 @@
include ':app', ':armadillo'
include ':app', ':armadillo', ':armadillo-datastore'

0 comments on commit b9c674a

Please sign in to comment.