Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jetpack DataStore Integrations. #54

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.30")

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

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

testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.2")
androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
androidTestImplementation("org.bouncycastle:bcprov-jdk15on:1.67")
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,58 @@
package at.favre.lib.armadillo.datastore

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

@ExperimentalSerializationApi
class UserStore(private val context: Context) {
companion object {
private const val fileName = "user"
}
private val serializer = User.serializer()
private val protobuf = ProtoBuf {}

private val protocol = object : ProtobufProtocol<User> {

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

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

override fun encode(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 = fileName,
serializer = userSerializer
)

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

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

fun clear() {
with(context) {
val dataStore = File(this.filesDir, "datastore/$fileName")
if(dataStore.exists()) {
dataStore.delete()
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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.Before
import org.junit.Test
import org.junit.runner.RunWith

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

@Before
fun setup() {
val store = UserStore(ApplicationProvider.getApplicationContext())
store.clear()
}

@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,130 @@
package at.favre.lib.armadillo.datastore

import android.content.Context
import android.os.Build
import androidx.datastore.core.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

class ArmadilloSerializer<T>(
context: Context,
private val protocol: ProtobufProtocol<T>,
password: CharArray? = null,
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 serializerPassword: ByteArrayRuntimeObfuscator?
private val encryptionProtocol: EncryptionProtocol
private val fingerprint: EncryptionFingerprint = EncryptionFingerprintFactory.create(
context,
buildString { fingerprintData.forEach(::append) }
)
private val defaultConfig = EncryptionProtocolConfig.newDefaultConfig()
private val kitKatConfig by lazy {
@Suppress("DEPRECATION")
EncryptionProtocolConfig.newBuilder(defaultConfig.build())
.authenticatedEncryption(AesCbcEncryption(secureRandom, provider))
.protocolVersion(Armadillo.KITKAT_PROTOCOL_VERSION)
.build()
}

init {

val stringMessageDigest = HkdfMessageDigest(
BuildConfig.PREF_SALT,
CONTENT_KEY_OUT_BYTE_LENGTH
)

val config =
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
kitKatConfig
} else {
EncryptionProtocolConfig
.newBuilder(defaultConfig.build())
.authenticatedEncryption(AesGcmEncryption(secureRandom, provider))
.build()
}
checkKitKatSupport(config.authenticatedEncryption)

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

encryptionProtocol = factory.create(preferencesSalt)
serializerPassword = password?.let(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 = "ArmadilloStoreSerializer"
}


private fun encrypt(content: ByteArray): ByteArray = with(encryptionProtocol) {
encrypt(
deriveContentKey(CRYPTO_KEY),
deobfuscatePassword(serializerPassword),
content
)
}


private fun decrypt(encrypted: ByteArray): ByteArray? =
if (encrypted.isEmpty()) {
null
} else {
encryptionProtocol
.decrypt(
encryptionProtocol.deriveContentKey(CRYPTO_KEY),
encryptionProtocol.deobfuscatePassword(serializerPassword),
encrypted
)
}

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


override fun writeTo(t: T, output: OutputStream) {
protocol
.encode(t)
.let(::encrypt)
.also(output::write)
}

override val defaultValue: T
get() = protocol.default()
}
Loading