From fe6190eb8d8959bd2c6ef8e2b07997cbabd752b0 Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 12 Mar 2025 23:00:40 +0300 Subject: [PATCH] +EncryptedKVStorage --- build.gradle.kts | 2 +- .../sergeych/crypto2/EncryptedKVStorage.kt | 86 +++++++++++++++++++ src/commonTest/kotlin/BinaryIdTest.kt | 8 +- src/commonTest/kotlin/StorageTest.kt | 70 +++++++++++++++ 4 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/EncryptedKVStorage.kt create mode 100644 src/commonTest/kotlin/StorageTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 2f1a618..68f1e52 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -72,7 +72,7 @@ kotlin { implementation(project.dependencies.platform("org.kotlincrypto.hash:bom:0.5.1")) implementation("org.kotlincrypto.hash:sha3") api("com.ionspin.kotlin:bignum:0.3.9") - api("net.sergeych:mp_bintools:0.1.11-SNAPSHOT") + api("net.sergeych:mp_bintools:0.1.12-SNAPSHOT") api("net.sergeych:mp_stools:1.5.1") } } diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/EncryptedKVStorage.kt b/src/commonMain/kotlin/net/sergeych/crypto2/EncryptedKVStorage.kt new file mode 100644 index 0000000..5c461c5 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/EncryptedKVStorage.kt @@ -0,0 +1,86 @@ +package net.sergeych.crypto2 + +import net.sergeych.bintools.KVStorage +import net.sergeych.bintools.MemoryKVStorage +import net.sergeych.bintools.optStored +import net.sergeych.synctools.ProtectedOp +import net.sergeych.synctools.invoke +import kotlin.random.Random +import kotlin.random.nextUBytes + +/** + * Encrypted variant of [KVStorage]; the storage is encrypted with the given key + * in a given [plainStore] [KVStorage]. It is threadsafe where + * applicable. Also, it supports in-place key change [reEncrypt]. + * + * Keys are stored encrypted and used hashed so it is not possible to + * retrieve them without knowing the encryption key. + */ +class EncryptedKVStorage( + private val plainStore: KVStorage, + private var encryptionKey: SymmetricKey, + private val prefix: String = "EKVS_" +) : KVStorage { + private val op = ProtectedOp() + + private val prefix2 = prefix + ":" + + val seed: UByteArray + + init { + var encryptedSeed by plainStore.optStored("$prefix#seed") + seed = encryptedSeed?.let { encryptionKey.decrypt(it) } + ?: Random.nextUBytes(32).also { + encryptedSeed = encryptionKey.encrypt(it) + } + } + + private fun mkkey(key: String): String = + blake2b(key.encodeToByteArray().asUByteArray() + seed).encodeToBase64Url() + + override val keys: Set + get() = op.invoke { + plainStore.keys.mapNotNull { + if (it.startsWith(prefix2)) + plainStore[it]?.let { encrypted -> + encryptionKey.decrypt(encrypted.asUByteArray()).asByteArray().decodeToString() + } + else null + }.toSet() + } + + override fun get(key: String): ByteArray? = op { + val k0 = mkkey(key) + val k = prefix + k0 + plainStore[k]?.let { encryptionKey.decrypt(it.asUByteArray()).asByteArray() } + ?.also { + val k2 = prefix2 + k0 + if (k2 !in plainStore) + plainStore[k2] = encryptionKey.encrypt(key).asByteArray() + } + } + + override fun set(key: String, value: ByteArray?) { + op { + val k1 = mkkey(key) + plainStore[prefix + k1] = value?.let { + encryptionKey.encrypt(it.asUByteArray()).asByteArray() + } + plainStore[prefix2 + k1] = encryptionKey.encrypt(key).asByteArray() + } + } + + /** + * Re-encrypts the entire storage in-place with the given key; it is threadsafe where + * applicable. + * + * This method re-encrypts every data item so it is cryptographically secure. + */ + fun reEncrypt(newKey: SymmetricKey) { + op { + val copy = MemoryKVStorage().also { it.addAll(this) } + encryptionKey = newKey + addAll(copy) + } + } +} \ No newline at end of file diff --git a/src/commonTest/kotlin/BinaryIdTest.kt b/src/commonTest/kotlin/BinaryIdTest.kt index e00f50a..8ae5019 100644 --- a/src/commonTest/kotlin/BinaryIdTest.kt +++ b/src/commonTest/kotlin/BinaryIdTest.kt @@ -9,10 +9,10 @@ */ import kotlinx.coroutines.test.runTest +import net.sergeych.bintools.ByteChunk import net.sergeych.bintools.toDump import net.sergeych.bipack.BipackEncoder import net.sergeych.crypto2.BinaryId -import net.sergeych.crypto2.ByteChunk import net.sergeych.crypto2.initCrypto import kotlin.test.Test import kotlin.test.assertContentEquals @@ -35,10 +35,10 @@ class BinaryIdTest { initCrypto() val x = ByteChunk.random(3) assertEquals(3, x.data.size) - assertEquals(3, x.toByteArray().size) - assertEquals(3, x.toUByteArray().size) + assertEquals(3, x.asByteArray.size) + assertEquals(3, x.data.size) println(BipackEncoder.encode(x).toDump()) assertEquals(4, BipackEncoder.encode(x).size) - assertContentEquals(BipackEncoder.encode(x.toByteArray()), BipackEncoder.encode(x)) + assertContentEquals(BipackEncoder.encode(x.asByteArray), BipackEncoder.encode(x)) } } \ No newline at end of file diff --git a/src/commonTest/kotlin/StorageTest.kt b/src/commonTest/kotlin/StorageTest.kt new file mode 100644 index 0000000..ab8a5ed --- /dev/null +++ b/src/commonTest/kotlin/StorageTest.kt @@ -0,0 +1,70 @@ +import kotlinx.coroutines.test.runTest +import net.sergeych.bintools.* +import net.sergeych.bipack.decodeFromBipack +import net.sergeych.crypto2.EncryptedKVStorage +import net.sergeych.crypto2.SymmetricKey +import net.sergeych.crypto2.initCrypto +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class StorageTest { + + @Test + fun testGetAndSet() = runTest { + initCrypto() + val plain = MemoryKVStorage() + val key = SymmetricKey.new() + val storage = EncryptedKVStorage(plain, key) + + var hello by storage.optStored() + assertNull(hello) + hello = "world" + assertEquals("world", storage["hello"]?.decodeFromBipack()) + println("plain: ${plain.keys}") + assertEquals(setOf("hello"), storage.keys) + var foo by storage.stored("bar") + assertEquals("bar", foo) + foo = "bar2" +// plain.dump() +// storage.dump() + assertEquals(setOf("hello", "foo"), storage.keys) + } + + @Test + fun testReEncrypt() = runTest { + initCrypto() + fun test(x: KVStorage) { + val foo by x.stored("1") + val bar by x.stored("2") + val bazz by x.stored("3") + assertEquals("foo", foo) + assertEquals("bar", bar) + assertEquals("bazz", bazz) + } + fun setup(s: KVStorage, k: SymmetricKey): EncryptedKVStorage { + val x = EncryptedKVStorage(s, k) + var foo by x.stored("1") + var bar by x.stored("2") + var bazz by x.stored("3") + foo = "foo" + bar = "bar" + bazz = "bazz" + return x + } + val k1 = SymmetricKey.new() + val k2 = SymmetricKey.new() + val plain = MemoryKVStorage() + val s1 = setup(plain, k1) + test(s1) + s1.reEncrypt(k2) + test(s1) +// val s2 = EncryptedKVStorage(plain, k2) +// test(s2) + } +} + +fun KVStorage.dump() { + for (k in keys) + println("$k: ${this[k]?.toDump()}") +} \ No newline at end of file