From 3972694d9db68673c37bd19038b4b71feb06e1e5 Mon Sep 17 00:00:00 2001 From: Mitchell Tilbrook Date: Wed, 14 Oct 2020 09:55:55 +1100 Subject: [PATCH 1/4] Open core module more to add DataStore Not sure this is the best approach, it will do for now until a better way comes around. --- armadillo/build.gradle | 4 ++-- .../main/java/at/favre/lib/armadillo/AesCbcEncryption.java | 2 +- .../main/java/at/favre/lib/armadillo/AesGcmEncryption.java | 2 +- .../at/favre/lib/armadillo/DefaultEncryptionProtocol.java | 4 ++-- .../main/java/at/favre/lib/armadillo/EncryptionProtocol.java | 2 +- .../main/java/at/favre/lib/armadillo/HkdfMessageDigest.java | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/armadillo/build.gradle b/armadillo/build.gradle index 8c244a3..1df9f77 100644 --- a/armadillo/build.gradle +++ b/armadillo/build.gradle @@ -60,8 +60,8 @@ dependencies { androidTestImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' androidTestImplementation 'org.mindrot:jbcrypt:0.4' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation(group: 'androidx.test.espresso', name: 'espresso-core', version: rootProject.ext.dependencies.espresso, { exclude group: 'com.android.support', module: 'support-annotations' }) diff --git a/armadillo/src/main/java/at/favre/lib/armadillo/AesCbcEncryption.java b/armadillo/src/main/java/at/favre/lib/armadillo/AesCbcEncryption.java index 61ef214..9fd2215 100644 --- a/armadillo/src/main/java/at/favre/lib/armadillo/AesCbcEncryption.java +++ b/armadillo/src/main/java/at/favre/lib/armadillo/AesCbcEncryption.java @@ -39,7 +39,7 @@ */ @SuppressWarnings({"WeakerAccess", "DeprecatedIsStillUsed"}) @Deprecated -final class AesCbcEncryption implements AuthenticatedEncryption { +public final class AesCbcEncryption implements AuthenticatedEncryption { private static final String ALGORITHM = "AES/CBC/PKCS5Padding"; private static final String HMAC_ALGORITHM = "HmacSHA256"; private static final int IV_LENGTH_BYTE = 16; diff --git a/armadillo/src/main/java/at/favre/lib/armadillo/AesGcmEncryption.java b/armadillo/src/main/java/at/favre/lib/armadillo/AesGcmEncryption.java index b98bad7..58d3a7e 100644 --- a/armadillo/src/main/java/at/favre/lib/armadillo/AesGcmEncryption.java +++ b/armadillo/src/main/java/at/favre/lib/armadillo/AesGcmEncryption.java @@ -32,7 +32,7 @@ * @since 18.12.2017 */ @SuppressWarnings("WeakerAccess") -final class AesGcmEncryption implements AuthenticatedEncryption { +public final class AesGcmEncryption implements AuthenticatedEncryption { private static final String ALGORITHM = "AES/GCM/NoPadding"; private static final int TAG_LENGTH_BIT = 128; private static final int IV_LENGTH_BYTE = 12; diff --git a/armadillo/src/main/java/at/favre/lib/armadillo/DefaultEncryptionProtocol.java b/armadillo/src/main/java/at/favre/lib/armadillo/DefaultEncryptionProtocol.java index 129e229..5034742 100644 --- a/armadillo/src/main/java/at/favre/lib/armadillo/DefaultEncryptionProtocol.java +++ b/armadillo/src/main/java/at/favre/lib/armadillo/DefaultEncryptionProtocol.java @@ -32,7 +32,7 @@ * @since 18.12.2017 */ -final class DefaultEncryptionProtocol implements EncryptionProtocol { +public final class DefaultEncryptionProtocol implements EncryptionProtocol { private static final int CONTENT_SALT_LENGTH_BYTES = 16; private static final int STRETCHED_PASSWORD_LENGTH_BYTES = 32; @@ -236,7 +236,7 @@ public static final class Factory implements EncryptionProtocol.Factory { private EncryptionProtocolConfig defaultConfig; private final List additionalDecryptionConfigs; - Factory(EncryptionProtocolConfig defaultConfig, EncryptionFingerprint fingerprint, + public Factory(EncryptionProtocolConfig defaultConfig, EncryptionFingerprint fingerprint, StringMessageDigest stringMessageDigest, SecureRandom secureRandom, boolean enableDerivedPasswordCaching, List additionalDecryptionConfigs) { diff --git a/armadillo/src/main/java/at/favre/lib/armadillo/EncryptionProtocol.java b/armadillo/src/main/java/at/favre/lib/armadillo/EncryptionProtocol.java index 1d33f80..cc360ff 100644 --- a/armadillo/src/main/java/at/favre/lib/armadillo/EncryptionProtocol.java +++ b/armadillo/src/main/java/at/favre/lib/armadillo/EncryptionProtocol.java @@ -15,7 +15,7 @@ * @since 18.12.2017 */ -interface EncryptionProtocol { +public interface EncryptionProtocol { /** * Given a original content key provided by the user creates a message digest of this key diff --git a/armadillo/src/main/java/at/favre/lib/armadillo/HkdfMessageDigest.java b/armadillo/src/main/java/at/favre/lib/armadillo/HkdfMessageDigest.java index 5bcbe65..eee1dcd 100644 --- a/armadillo/src/main/java/at/favre/lib/armadillo/HkdfMessageDigest.java +++ b/armadillo/src/main/java/at/favre/lib/armadillo/HkdfMessageDigest.java @@ -12,7 +12,7 @@ * @author Patrick Favre-Bulle */ -final class HkdfMessageDigest implements StringMessageDigest { +public final class HkdfMessageDigest implements StringMessageDigest { private final byte[] salt; private final int outLength; @@ -22,7 +22,7 @@ final class HkdfMessageDigest implements StringMessageDigest { * @param salt used in all derivations * @param outByteLength the byte length created by the derive function */ - HkdfMessageDigest(byte[] salt, int outByteLength) { + public HkdfMessageDigest(byte[] salt, int outByteLength) { this.salt = Objects.requireNonNull(salt); this.outLength = outByteLength; } From 076b64c9c8f4ef0f3af986968df77c901a8faa3e Mon Sep 17 00:00:00 2001 From: Mitchell Tilbrook Date: Thu, 15 Oct 2020 09:52:56 +1100 Subject: [PATCH 2/4] Initial encrypted datastore This is very much a first draft as we need to workout how this and the core module work together better. --- armadillo-datastore/.gitignore | 1 + armadillo-datastore/README.md | 62 ++++++++ armadillo-datastore/build.gradle.kts | 65 ++++++++ armadillo-datastore/consumer-rules.pro | 0 armadillo-datastore/proguard-rules.pro | 0 .../at/favre/lib/armadillo/datastore/User.kt | 9 ++ .../lib/armadillo/datastore/UserStore.kt | 45 ++++++ .../lib/armadillo/datastore/UserStoreTest.kt | 41 +++++ .../src/main/AndroidManifest.xml | 2 + .../datastore/ArmadilloSerializer.kt | 142 ++++++++++++++++++ build.gradle | 4 +- settings.gradle | 2 +- 12 files changed, 369 insertions(+), 4 deletions(-) create mode 100644 armadillo-datastore/.gitignore create mode 100644 armadillo-datastore/README.md create mode 100644 armadillo-datastore/build.gradle.kts create mode 100644 armadillo-datastore/consumer-rules.pro create mode 100644 armadillo-datastore/proguard-rules.pro create mode 100644 armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/User.kt create mode 100644 armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStore.kt create mode 100644 armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStoreTest.kt create mode 100644 armadillo-datastore/src/main/AndroidManifest.xml create mode 100644 armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ArmadilloSerializer.kt diff --git a/armadillo-datastore/.gitignore b/armadillo-datastore/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/armadillo-datastore/.gitignore @@ -0,0 +1 @@ +/build diff --git a/armadillo-datastore/README.md b/armadillo-datastore/README.md new file mode 100644 index 0000000..2469840 --- /dev/null +++ b/armadillo-datastore/README.md @@ -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; +} +``` diff --git a/armadillo-datastore/build.gradle.kts b/armadillo-datastore/build.gradle.kts new file mode 100644 index 0000000..1c83e70 --- /dev/null +++ b/armadillo-datastore/build.gradle.kts @@ -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") +} diff --git a/armadillo-datastore/consumer-rules.pro b/armadillo-datastore/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/armadillo-datastore/proguard-rules.pro b/armadillo-datastore/proguard-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/User.kt b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/User.kt new file mode 100644 index 0000000..43c2256 --- /dev/null +++ b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/User.kt @@ -0,0 +1,9 @@ +package at.favre.lib.armadillo.datastore + +import kotlinx.serialization.Serializable + +@Serializable +data class User( + val name: String, + val email: String, +) diff --git a/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStore.kt b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStore.kt new file mode 100644 index 0000000..cfebfc8 --- /dev/null +++ b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStore.kt @@ -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 { + + 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 = + ArmadilloSerializer( + context = context, + protocol = protocol + ) + + private val store: DataStore = context.createDataStore( + fileName = "user.pb", + serializer = userSerializer + ) + + suspend fun update(reduce: (User) -> User) { + store.updateData { user -> reduce(user) } + } + + val user: Flow + get() = store.data + +} diff --git a/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStoreTest.kt b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStoreTest.kt new file mode 100644 index 0000000..897b420 --- /dev/null +++ b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStoreTest.kt @@ -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 == "") + } +} diff --git a/armadillo-datastore/src/main/AndroidManifest.xml b/armadillo-datastore/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5812320 --- /dev/null +++ b/armadillo-datastore/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ArmadilloSerializer.kt b/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ArmadilloSerializer.kt new file mode 100644 index 0000000..72311cc --- /dev/null +++ b/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ArmadilloSerializer.kt @@ -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 { + fun toBytes(data: T): ByteArray + fun fromBytes(bytes: ByteArray): T + fun fromNothing(): T +} + +class ArmadilloSerializer( + context: Context, + private val protocol: ProtobufProtocol, + fingerprintData: List = emptyList(), + secureRandom: SecureRandom = SecureRandom(), + additionalDecryptionConfigs: List = listOf(), + enabledKitkatSupport: Boolean = false, + provider: Provider? = null, + preferencesSalt: ByteArray = BuildConfig.PREF_SALT +) : Serializer { + + 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) + } +} diff --git a/build.gradle b/build.gradle index 71a2bc7..ad2590b 100644 --- a/build.gradle +++ b/build.gradle @@ -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" } } diff --git a/settings.gradle b/settings.gradle index ba3044e..28c7476 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':armadillo' +include ':app', ':armadillo', ':armadillo-datastore' From 77b1f82fc9ebd4e392577a5ada49bb99074fc993 Mon Sep 17 00:00:00 2001 From: Mitchell Tilbrook Date: Sun, 28 Feb 2021 08:49:22 +1100 Subject: [PATCH 3/4] WIP Commit while getting new computer setup --- .../lib/armadillo/datastore/UserStore.kt | 23 +++-- .../lib/armadillo/datastore/UserStoreTest.kt | 7 ++ .../datastore/ArmadilloSerializer.kt | 85 ++++++++----------- .../armadillo/datastore/ProtobufProtocol.kt | 27 ++++++ 4 files changed, 87 insertions(+), 55 deletions(-) create mode 100644 armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ProtobufProtocol.kt diff --git a/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStore.kt b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStore.kt index cfebfc8..3451856 100644 --- a/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStore.kt +++ b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStore.kt @@ -6,21 +6,25 @@ import androidx.datastore.createDataStore import kotlinx.coroutines.flow.Flow import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.protobuf.ProtoBuf +import java.io.File @ExperimentalSerializationApi -class UserStore(context: Context) { +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 { - override fun fromBytes(bytes: ByteArray): User = + override fun decode(bytes: ByteArray): User = protobuf.decodeFromByteArray(serializer, bytes) - override fun fromNothing(): User = + override fun default(): User = User(name = "", email = "") - override fun toBytes(data: User): ByteArray = + override fun encode(data: User): ByteArray = protobuf.encodeToByteArray(serializer, data) } @@ -31,7 +35,7 @@ class UserStore(context: Context) { ) private val store: DataStore = context.createDataStore( - fileName = "user.pb", + fileName = fileName, serializer = userSerializer ) @@ -42,4 +46,13 @@ class UserStore(context: Context) { val user: Flow get() = store.data + fun clear() { + with(context) { + val dataStore = File(this.filesDir, "datastore/$fileName") + if(dataStore.exists()) { + dataStore.delete() + } + } + } + } diff --git a/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStoreTest.kt b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStoreTest.kt index 897b420..8b0e14f 100644 --- a/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStoreTest.kt +++ b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStoreTest.kt @@ -5,6 +5,7 @@ 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 @@ -12,6 +13,12 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class UserStoreTest { + @Before + fun setup() { + val store = UserStore(ApplicationProvider.getApplicationContext()) + store.clear() + } + @Test fun canReadEmptyUserFromStore() { val store = UserStore(ApplicationProvider.getApplicationContext()) diff --git a/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ArmadilloSerializer.kt b/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ArmadilloSerializer.kt index 72311cc..e622a84 100644 --- a/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ArmadilloSerializer.kt +++ b/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ArmadilloSerializer.kt @@ -2,7 +2,6 @@ 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 @@ -12,15 +11,10 @@ import java.io.OutputStream import java.security.Provider import java.security.SecureRandom -interface ProtobufProtocol { - fun toBytes(data: T): ByteArray - fun fromBytes(bytes: ByteArray): T - fun fromNothing(): T -} - class ArmadilloSerializer( context: Context, private val protocol: ProtobufProtocol, + password: CharArray? = null, fingerprintData: List = emptyList(), secureRandom: SecureRandom = SecureRandom(), additionalDecryptionConfigs: List = listOf(), @@ -29,36 +23,38 @@ class ArmadilloSerializer( preferencesSalt: ByteArray = BuildConfig.PREF_SALT ) : Serializer { - private val password: ByteArrayRuntimeObfuscator? + 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 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) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { kitKatConfig } else { EncryptionProtocolConfig .newBuilder(defaultConfig.build()) .authenticatedEncryption(AesGcmEncryption(secureRandom, provider)) .build() - }.also { checkKitKatSupport(it.authenticatedEncryption) } + } + checkKitKatSupport(config.authenticatedEncryption) val factory = DefaultEncryptionProtocol.Factory( config, @@ -74,7 +70,7 @@ class ArmadilloSerializer( ) encryptionProtocol = factory.create(preferencesSalt) - password = null // TODO Add password config factory.obfuscatePassword() + serializerPassword = password?.let(factory::obfuscatePassword) } @@ -86,56 +82,45 @@ class ArmadilloSerializer( } companion object { - private const val CRYPTO_KEY = "ArmadilloStore" + private const val CRYPTO_KEY = "ArmadilloStoreSerializer" + } + + + private fun encrypt(content: ByteArray): ByteArray = with(encryptionProtocol) { + encrypt( + deriveContentKey(CRYPTO_KEY), + deobfuscatePassword(serializerPassword), + content + ) } - private fun encrypt(content: ByteArray): ByteArray = - try { + private fun decrypt(encrypted: ByteArray): ByteArray? = + if (encrypted.isEmpty()) { + null + } else { encryptionProtocol - .encrypt( + .decrypt( encryptionProtocol.deriveContentKey(CRYPTO_KEY), - encryptionProtocol.deobfuscatePassword(password), - content + encryptionProtocol.deobfuscatePassword(serializerPassword), + encrypted ) - } 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) + if (bytes.isEmpty()) protocol.default() + else protocol.decode(bytes) } override fun writeTo(t: T, output: OutputStream) { protocol - .toBytes(t) + .encode(t) .let(::encrypt) .also(output::write) } diff --git a/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ProtobufProtocol.kt b/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ProtobufProtocol.kt new file mode 100644 index 0000000..85543ef --- /dev/null +++ b/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ProtobufProtocol.kt @@ -0,0 +1,27 @@ +package at.favre.lib.armadillo.datastore + +/** + * The protocol is used to wrap the encoded/decode for any protobuff implementation. + * + * Datastore requies the type T MUST be immutable. Any mutable types will result in a broken DataStore. + * + * Datastore will always return an empty + */ +interface ProtobufProtocol { + /** + * un-encrypted proto byte encoding of [T] + */ + fun encode(data: T): ByteArray + + /** + * un-encrypted proto bytes to proto class of [T] + */ + fun decode(bytes: ByteArray): T + + /** + * Returns a default value when the store is empty. This will + * always happen on first read even when writing to the store + * for the first time. + */ + fun default(): T +} From 684caea63d00725849a714b4a904ed49eaebeecd Mon Sep 17 00:00:00 2001 From: marukami Date: Tue, 9 Mar 2021 12:36:26 +1100 Subject: [PATCH 4/4] Upadate libs --- armadillo-datastore/build.gradle.kts | 10 +- .../lib/armadillo/datastore/UserStore.kt | 4 +- .../datastore/ArmadilloSerializer.kt | 205 +++++++++--------- .../armadillo/datastore/ProtobufProtocol.kt | 4 + build.gradle | 4 +- 5 files changed, 117 insertions(+), 110 deletions(-) diff --git a/armadillo-datastore/build.gradle.kts b/armadillo-datastore/build.gradle.kts index 1c83e70..4567742 100644 --- a/armadillo-datastore/build.gradle.kts +++ b/armadillo-datastore/build.gradle.kts @@ -47,18 +47,18 @@ android { dependencies { implementation(project(":armadillo")) - implementation("org.jetbrains.kotlin:kotlin-stdlib:1.4.10") + 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-alpha01") + implementation("androidx.datastore:datastore-core:1.0.0-alpha07") - androidTestImplementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.0.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.0.0") - testImplementation("junit:junit:4.13") + 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.60") + 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") diff --git a/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStore.kt b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStore.kt index 3451856..3dc7c38 100644 --- a/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStore.kt +++ b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStore.kt @@ -1,8 +1,8 @@ package at.favre.lib.armadillo.datastore import android.content.Context -import androidx.datastore.DataStore -import androidx.datastore.createDataStore +import androidx.datastore.core.DataStore +import androidx.datastore.core.createDataStore import kotlinx.coroutines.flow.Flow import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.protobuf.ProtoBuf diff --git a/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ArmadilloSerializer.kt b/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ArmadilloSerializer.kt index e622a84..7b08bfe 100644 --- a/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ArmadilloSerializer.kt +++ b/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ArmadilloSerializer.kt @@ -2,7 +2,7 @@ package at.favre.lib.armadillo.datastore import android.content.Context import android.os.Build -import androidx.datastore.Serializer +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 @@ -12,116 +12,119 @@ import java.security.Provider import java.security.SecureRandom class ArmadilloSerializer( - context: Context, - private val protocol: ProtobufProtocol, - password: CharArray? = null, - fingerprintData: List = emptyList(), - secureRandom: SecureRandom = SecureRandom(), - additionalDecryptionConfigs: List = listOf(), - enabledKitkatSupport: Boolean = false, - provider: Provider? = null, - preferencesSalt: ByteArray = BuildConfig.PREF_SALT + context: Context, + private val protocol: ProtobufProtocol, + password: CharArray? = null, + fingerprintData: List = emptyList(), + secureRandom: SecureRandom = SecureRandom(), + additionalDecryptionConfigs: List = listOf(), + enabledKitkatSupport: Boolean = false, + provider: Provider? = null, + preferencesSalt: ByteArray = BuildConfig.PREF_SALT ) : Serializer { - 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 + 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) + } - val config = - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - kitKatConfig - } else { - EncryptionProtocolConfig - .newBuilder(defaultConfig.build()) - .authenticatedEncryption(AesGcmEncryption(secureRandom, provider)) - .build() + + 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()") } - 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) - } + companion object { + private const val CRYPTO_KEY = "ArmadilloStoreSerializer" + } - 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()") + private fun encrypt(content: ByteArray): ByteArray = with(encryptionProtocol) { + encrypt( + deriveContentKey(CRYPTO_KEY), + deobfuscatePassword(serializerPassword), + content + ) } - } - companion object { - private const val CRYPTO_KEY = "ArmadilloStoreSerializer" - } + 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) + } - 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()) protocol.default() - 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() } diff --git a/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ProtobufProtocol.kt b/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ProtobufProtocol.kt index 85543ef..8f744e6 100644 --- a/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ProtobufProtocol.kt +++ b/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ProtobufProtocol.kt @@ -8,6 +8,10 @@ package at.favre.lib.armadillo.datastore * Datastore will always return an empty */ interface ProtobufProtocol { +// /** +// * If the file contents has change you must migrate the data +// */ +// val version: Int /** * un-encrypted proto byte encoding of [T] */ diff --git a/build.gradle b/build.gradle index ad2590b..517cf8a 100644 --- a/build.gradle +++ b/build.gradle @@ -7,12 +7,12 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:4.1.2' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' 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' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.30" } }