+EncryptedKVStorage

This commit is contained in:
Sergey Chernov 2025-03-12 23:00:40 +03:00
parent 776f4e75ff
commit fe6190eb8d
4 changed files with 161 additions and 5 deletions

View File

@ -72,7 +72,7 @@ kotlin {
implementation(project.dependencies.platform("org.kotlincrypto.hash:bom:0.5.1")) implementation(project.dependencies.platform("org.kotlincrypto.hash:bom:0.5.1"))
implementation("org.kotlincrypto.hash:sha3") implementation("org.kotlincrypto.hash:sha3")
api("com.ionspin.kotlin:bignum:0.3.9") 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") api("net.sergeych:mp_stools:1.5.1")
} }
} }

View File

@ -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<UByteArray>("$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<String>
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)
}
}
}

View File

@ -9,10 +9,10 @@
*/ */
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.bintools.ByteChunk
import net.sergeych.bintools.toDump import net.sergeych.bintools.toDump
import net.sergeych.bipack.BipackEncoder import net.sergeych.bipack.BipackEncoder
import net.sergeych.crypto2.BinaryId import net.sergeych.crypto2.BinaryId
import net.sergeych.crypto2.ByteChunk
import net.sergeych.crypto2.initCrypto import net.sergeych.crypto2.initCrypto
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertContentEquals import kotlin.test.assertContentEquals
@ -35,10 +35,10 @@ class BinaryIdTest {
initCrypto() initCrypto()
val x = ByteChunk.random(3) val x = ByteChunk.random(3)
assertEquals(3, x.data.size) assertEquals(3, x.data.size)
assertEquals(3, x.toByteArray().size) assertEquals(3, x.asByteArray.size)
assertEquals(3, x.toUByteArray().size) assertEquals(3, x.data.size)
println(BipackEncoder.encode(x).toDump()) println(BipackEncoder.encode(x).toDump())
assertEquals(4, BipackEncoder.encode(x).size) assertEquals(4, BipackEncoder.encode(x).size)
assertContentEquals(BipackEncoder.encode(x.toByteArray()), BipackEncoder.encode(x)) assertContentEquals(BipackEncoder.encode(x.asByteArray), BipackEncoder.encode(x))
} }
} }

View File

@ -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<String>()
assertNull(hello)
hello = "world"
assertEquals("world", storage["hello"]?.decodeFromBipack<String>())
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()}")
}