forked from sergeych/crypto2
		
	+EncryptedKVStorage
This commit is contained in:
		
							parent
							
								
									776f4e75ff
								
							
						
					
					
						commit
						fe6190eb8d
					
				@ -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")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								src/commonTest/kotlin/StorageTest.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/commonTest/kotlin/StorageTest.kt
									
									
									
									
									
										Normal 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()}")
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user