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..4567742 --- /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.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") +} 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..3dc7c38 --- /dev/null +++ b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStore.kt @@ -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 { + + 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 = + ArmadilloSerializer( + context = context, + protocol = protocol + ) + + private val store: DataStore = context.createDataStore( + fileName = fileName, + serializer = userSerializer + ) + + suspend fun update(reduce: (User) -> User) { + store.updateData { user -> reduce(user) } + } + + 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 new file mode 100644 index 0000000..8b0e14f --- /dev/null +++ b/armadillo-datastore/src/androidTest/java/at/favre/lib/armadillo/datastore/UserStoreTest.kt @@ -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 == "") + } +} 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..7b08bfe --- /dev/null +++ b/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ArmadilloSerializer.kt @@ -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( + 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 + ) + + 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() +} 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..8f744e6 --- /dev/null +++ b/armadillo-datastore/src/main/java/at/favre/lib/armadillo/datastore/ProtobufProtocol.kt @@ -0,0 +1,31 @@ +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 { +// /** +// * If the file contents has change you must migrate the data +// */ +// val version: Int + /** + * 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 +} 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; } diff --git a/build.gradle b/build.gradle index 71a2bc7..517cf8a 100644 --- a/build.gradle +++ b/build.gradle @@ -7,14 +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' - - // 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.30" } } 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'