forked from sergeych/crypto2
Compare commits
19
Commits
e2d4fb07ad
..
0.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13dff8d760 | ||
|
|
3bd06ac7ff | ||
|
|
ca9ab0f7a0 | ||
|
|
b86ac6f00b | ||
|
|
9d338d2f13 | ||
|
|
5228be33ee | ||
|
|
528439f61d | ||
|
|
7c97a843e7 | ||
|
|
1a81cd5110 | ||
|
|
7d3e396cf7 | ||
|
|
85b13ed8ca | ||
|
|
fbbe4d3a34 | ||
|
|
875c0f7a50 | ||
|
|
fe6190eb8d | ||
|
|
776f4e75ff | ||
|
|
fa7263b0e7 | ||
|
|
bd81f88dd8 | ||
|
|
6fcf7841a7 | ||
|
|
4748ea0d65 |
No files matched your search
@@ -9,6 +9,7 @@ build/
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
Generated
-7
@@ -1,10 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
/artifacts/crypto2_js_0_1_0_SNAPSHOT.xml
|
||||
/artifacts/crypto2_jvm_0_1_0_SNAPSHOT.xml
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="crypto2-js-0.1.1-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="crypto2-js-0.1.1-SNAPSHOT.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="crypto2-js-1.0-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="crypto2-js-1.0-SNAPSHOT.jar">
|
||||
<element id="module-output" name="crypto2.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="crypto2-jvm-0.1.1-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="crypto2-jvm-0.1.1-SNAPSHOT.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="crypto2-jvm-1.0-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="crypto2-jvm-1.0-SNAPSHOT.jar">
|
||||
<element id="module-output" name="crypto2.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
@@ -1,8 +0,0 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="crypto2-wasm-js-0.1.1-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="crypto2-wasm-js-0.1.1-SNAPSHOT.jar">
|
||||
<element id="module-output" name="crypto2.wasmJsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+24
@@ -1,5 +1,29 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<DBN-PSQL>
|
||||
<case-options enabled="true">
|
||||
<option name="KEYWORD_CASE" value="lower" />
|
||||
<option name="FUNCTION_CASE" value="lower" />
|
||||
<option name="PARAMETER_CASE" value="lower" />
|
||||
<option name="DATATYPE_CASE" value="lower" />
|
||||
<option name="OBJECT_CASE" value="preserve" />
|
||||
</case-options>
|
||||
<formatting-settings enabled="false" />
|
||||
</DBN-PSQL>
|
||||
<DBN-SQL>
|
||||
<case-options enabled="true">
|
||||
<option name="KEYWORD_CASE" value="lower" />
|
||||
<option name="FUNCTION_CASE" value="lower" />
|
||||
<option name="PARAMETER_CASE" value="lower" />
|
||||
<option name="DATATYPE_CASE" value="lower" />
|
||||
<option name="OBJECT_CASE" value="preserve" />
|
||||
</case-options>
|
||||
<formatting-settings enabled="false">
|
||||
<option name="STATEMENT_SPACING" value="one_line" />
|
||||
<option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
|
||||
<option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
|
||||
</formatting-settings>
|
||||
</DBN-SQL>
|
||||
<ScalaCodeStyleSettings>
|
||||
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
|
||||
</ScalaCodeStyleSettings>
|
||||
|
||||
Generated
+2
@@ -5,6 +5,7 @@
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="/usr/local/Cellar/gradle/7.6/libexec" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
@@ -12,5 +13,6 @@
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
<option name="parallelModelFetch" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ReplaceUntilWithRangeUntil" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="StructuralWrap" enabled="false" level="TYPO" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.20" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+2
-1
@@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17 (5)" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="corretto-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -2,6 +2,16 @@
|
||||
|
||||
Kotlin Multiplatform cryptographic primitives using modern strong cryptography.
|
||||
|
||||
## v0.9.0 for kotlin 2.2.21, new kotlin time compatible
|
||||
|
||||
The primary goal was to fix kotlin-caused incompatibilities with kotlinx.datetime.Instant and Clock; the upgrade shoud be in-place
|
||||
replacement providing calling code ises `kotlin.time.Instant` and
|
||||
`kotlin.time.Clock` respectively. No other changes are needed.
|
||||
|
||||
Also we start to add small syntax sugar methods.
|
||||
|
||||
## v.0.8.4 is built for all platform, IOS and wasmJS included
|
||||
|
||||
Cryptographic API works exactly the same and compiles to any platform supported listed below with no change in source code.
|
||||
|
||||
All primitives meant to send over the network or store are `kotlinx.serialization` compatible, serializers included.
|
||||
@@ -21,7 +31,7 @@ repositories {
|
||||
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
||||
}
|
||||
dependencies {
|
||||
import("net.sergeych:crypto2:0.7.1-SNAPSHOT")
|
||||
import("net.sergeych:crypto2:0.8.4")
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+27
-36
@@ -9,33 +9,30 @@
|
||||
*/
|
||||
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform") version "2.0.20"
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20"
|
||||
kotlin("multiplatform") version "2.2.21"
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.21"
|
||||
id("org.jetbrains.dokka") version "1.9.20"
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "0.7.4-SNAPSHOT"
|
||||
version = "0.9.0"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://maven.universablockchain.com/")
|
||||
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
||||
// maven("https://gitea.sergeych.net/api/packages/YoungBlood/maven")
|
||||
maven("https://gitea.sergeych.net/api/packages/YoungBlood/maven")
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm {
|
||||
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_11
|
||||
}
|
||||
}
|
||||
jvmToolchain(21)
|
||||
jvm()
|
||||
js {
|
||||
browser()
|
||||
nodejs()
|
||||
@@ -49,38 +46,37 @@ kotlin {
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
mingwX64()
|
||||
// @OptIn(ExperimentalWasmDsl::class)
|
||||
// wasmJs {
|
||||
// browser()
|
||||
// }
|
||||
val ktor_version = "2.3.6"
|
||||
@OptIn(ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
all {
|
||||
languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi")
|
||||
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
|
||||
languageSettings.optIn("kotlin.time.ExperimentalTime")
|
||||
}
|
||||
|
||||
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||
|
||||
implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.2")
|
||||
implementation("net.sergeych:multiplatform-crypto-libsodium-bindings:0.9.6")
|
||||
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.7")
|
||||
api("net.sergeych:mp_stools:1.5.1")
|
||||
api("com.ionspin.kotlin:bignum:0.3.10")
|
||||
api("net.sergeych:mp_bintools:0.3.2")
|
||||
|
||||
}
|
||||
}
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
implementation("org.slf4j:slf4j-simple:2.0.9")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
|
||||
}
|
||||
}
|
||||
val native by creating {
|
||||
@@ -95,19 +91,6 @@ kotlin {
|
||||
val jvmTest by getting
|
||||
for (platform in listOf(linuxX64Main, linuxArm64Main, macosX64Main, macosArm64Main, iosX64Main, iosArm64Main, iosSimulatorArm64Main, mingwX64Main))
|
||||
platform { dependsOn(native) }
|
||||
|
||||
// val wasmJsMain by getting {
|
||||
// val wasmJsTargetRegex = Regex(pattern = "wasmJs.*")
|
||||
// configurations.all {
|
||||
// if (wasmJsTargetRegex.containsMatchIn(input = this.name)) {
|
||||
// resolutionStrategy.dependencySubstitution {
|
||||
// substitute(module("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.2"))
|
||||
// .using(module("net.sergeych:multiplatform-crypto-libsodium-bindings:0.9.4-SNAPSHOT"))
|
||||
// .withoutClassifier()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +112,14 @@ publishing {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<Test>("jvmTest") {
|
||||
// Ignore Kotlin synthetic classes generated from files that look like tests
|
||||
exclude("**/*TestKt.class")
|
||||
exclude("**/*TestsKt.class")
|
||||
exclude("**/*AssertThrowsKt.class")
|
||||
exclude("**/*Test_toolsKt.class")
|
||||
}
|
||||
|
||||
tasks.dokkaHtml.configure {
|
||||
outputDirectory.set(buildDir.resolve("dokka"))
|
||||
dokkaSourceSets {
|
||||
|
||||
@@ -9,3 +9,7 @@
|
||||
#
|
||||
|
||||
kotlin.code.style=official
|
||||
org.gradle.parallel=true
|
||||
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.caching=true
|
||||
+1
-1
@@ -10,6 +10,6 @@
|
||||
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
||||
*
|
||||
* You may use, distribute and modify this code under the
|
||||
* terms of the private license, which you must obtain from the author
|
||||
*
|
||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
||||
* real dot sergeych at gmail.
|
||||
*/
|
||||
|
||||
package net.sergeych.crypto2
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.sergeych.bintools.decodeHex
|
||||
import net.sergeych.bintools.encodeToHex
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Bytes sequence with comparison, concatenation, and string representation,
|
||||
* could be used as hash keys for pure binary values, etc.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
@Serializable
|
||||
class ByteChunk(val data: UByteArray): Comparable<ByteChunk> {
|
||||
|
||||
val size: Int get() = data.size
|
||||
|
||||
/**
|
||||
* Per-byte comparison also of different length. From two chunks
|
||||
* of different size but equal beginning, the shorter is considered
|
||||
* the smaller.
|
||||
*/
|
||||
override fun compareTo(other: ByteChunk): Int {
|
||||
val limit = min(size, other.size)
|
||||
for( i in 0 ..< limit) {
|
||||
val own = data[i]
|
||||
val their = other.data[i]
|
||||
if( own < their) return -1
|
||||
else if( own > their) return 1
|
||||
}
|
||||
if( size < other.size ) return -1
|
||||
if( size > other.size ) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Equal chunks means content equality.
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ByteChunk) return false
|
||||
|
||||
return data contentEquals other.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Content-based hash code
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
return data.contentHashCode()
|
||||
}
|
||||
|
||||
/**
|
||||
* hex representation of data
|
||||
*/
|
||||
override fun toString(): String = base64Url
|
||||
|
||||
/**
|
||||
* Hex encoded data
|
||||
*/
|
||||
val hex by lazy { data.encodeToHex() }
|
||||
|
||||
val base64Url by lazy { data.encodeToBase64Url() }
|
||||
|
||||
/**
|
||||
* human-readable dump
|
||||
*/
|
||||
val dump by lazy { data.toDump() }
|
||||
|
||||
/**
|
||||
* Concatenate two chunks and return new one
|
||||
*/
|
||||
operator fun plus(other: ByteChunk): ByteChunk = ByteChunk(data + other.data)
|
||||
|
||||
fun toByteArray(): ByteArray = data.asByteArray()
|
||||
fun toUByteArray(): UByteArray = data
|
||||
|
||||
companion object {
|
||||
fun fromHex(hex: String): ByteChunk = ByteChunk(hex.decodeHex().asUByteArray())
|
||||
fun random(sizeInBytes: Int=16) = randomUBytes(sizeInBytes).toChunk()
|
||||
}
|
||||
}
|
||||
|
||||
private fun UByteArray.toChunk(): ByteChunk = ByteChunk(this)
|
||||
@Suppress("unused")
|
||||
private fun ByteArray.toChunk(): ByteChunk = ByteChunk(this.asUByteArray())
|
||||
|
||||
@Suppress("unused")
|
||||
fun ByteArray.asChunk() = ByteChunk(toUByteArray())
|
||||
fun UByteArray.asChunk(): ByteChunk = ByteChunk(this)
|
||||
@@ -321,6 +321,27 @@ sealed class Container {
|
||||
}.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the container with a password. It scans all key ids for
|
||||
* these with `KDP` params, e.g., derived from password, and try to
|
||||
* derive keys from the password and decrypt the container. If there are
|
||||
* no derivable keys, or all of them failed to decrypt, returns null.
|
||||
* It could be long operation if there are multiple derivable keys with heavy
|
||||
* KDF. See [PBKD] and [KDF] for more.
|
||||
*
|
||||
* @return decrypted data or null
|
||||
*/
|
||||
@Suppress("unused")
|
||||
fun decryptWithPassword(password: String): UByteArray? {
|
||||
for( id in this.keyIds ) {
|
||||
id.kdp?.let { kdp ->
|
||||
decryptWith(kdp.deriveKey(password))?.let { return it }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
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.
|
||||
*
|
||||
* @param plainStore where to store encrypted data
|
||||
* @param encryptionKey key to decrypt existing/encrypt new data. Can cause [DecryptionFailedException]
|
||||
* if the key is wrong and the storage is already initialized with a new key and same [prefix]
|
||||
* @param prefix prefix for keys to distinguish from other data in [plainStore]
|
||||
* @param removeExisting if true, removes all existing data in [plainStore] if the [encryptionKey] can't
|
||||
* decrypt existing encrypted data
|
||||
*/
|
||||
class EncryptedKVStorage(
|
||||
private val plainStore: KVStorage,
|
||||
private var encryptionKey: SymmetricKey,
|
||||
private val prefix: String = "EKVS_",
|
||||
removeExisting: Boolean
|
||||
) : KVStorage {
|
||||
private val op = ProtectedOp()
|
||||
|
||||
private val prefix2 = prefix + ":"
|
||||
|
||||
val seed: UByteArray
|
||||
|
||||
init {
|
||||
var encryptedSeed by plainStore.optStored<UByteArray>("$prefix#seed")
|
||||
seed = try {
|
||||
encryptedSeed?.let { encryptionKey.decrypt(it) }
|
||||
?: Random.nextUBytes(32).also {
|
||||
encryptedSeed = encryptionKey.encrypt(it)
|
||||
}
|
||||
} catch (x: DecryptionFailedException) {
|
||||
if (removeExisting) {
|
||||
plainStore.keys.filter { it.startsWith(prefix) }.forEach {
|
||||
plainStore.delete(it)
|
||||
}
|
||||
Random.nextUBytes(32).also {
|
||||
encryptedSeed = encryptionKey.encrypt(it)
|
||||
}
|
||||
} else throw x
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import com.ionspin.kotlin.crypto.generichash.GenericHash
|
||||
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import net.sergeych.bintools.ByteChunk
|
||||
import net.sergeych.bintools.asChunk
|
||||
import org.kotlincrypto.hash.sha3.SHA3_256
|
||||
import org.kotlincrypto.hash.sha3.SHA3_384
|
||||
|
||||
|
||||
@@ -60,6 +60,12 @@ sealed class Multikey {
|
||||
*/
|
||||
abstract fun check(keys: Iterable<VerifyingPublicKey>): Boolean
|
||||
|
||||
/**
|
||||
* Step towards automated picking necessary keys.
|
||||
* Return all the keys mentioned in this multikey condition.
|
||||
*/
|
||||
abstract fun mentionedKeys(): Set<VerifyingPublicKey>
|
||||
|
||||
/**
|
||||
* Check that [verifyingKeys] satisfy the multikey condition
|
||||
*/
|
||||
@@ -97,6 +103,10 @@ sealed class Multikey {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun mentionedKeys(): Set<VerifyingPublicKey> {
|
||||
return validKeys
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +129,10 @@ sealed class Multikey {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun mentionedKeys(): Set<VerifyingPublicKey> {
|
||||
return validKeys.flatMap { it.mentionedKeys() }.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,6 +143,7 @@ sealed class Multikey {
|
||||
@Serializable
|
||||
object AnyKey : Multikey() {
|
||||
override fun check(keys: Iterable<VerifyingPublicKey>): Boolean = true
|
||||
override fun mentionedKeys(): Set<VerifyingPublicKey> = emptySet()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
package net.sergeych.crypto2
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.sergeych.bipack.BipackEncoder
|
||||
import net.sergeych.bipack.decodeFromBipack
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
package net.sergeych.crypto2
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import net.sergeych.bipack.BipackDecoder
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
package net.sergeych.crypto2
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Instant
|
||||
|
||||
interface SigningKey: KeyInstance {
|
||||
val verifyingKey: VerifyingPublicKey
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
package net.sergeych.crypto2
|
||||
|
||||
import com.ionspin.kotlin.crypto.signature.Signature
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
|
||||
@@ -44,7 +44,8 @@ sealed class KDF {
|
||||
*
|
||||
* Random salt of proper size is used
|
||||
*/
|
||||
fun kdfForSize(numberOfKeys: Int): KDF = creteDefault(SymmetricKey.keyLength * numberOfKeys, this)
|
||||
fun kdfForSize(numberOfKeys: Int,salt: UByteArray = Argon.randomSalt()): KDF =
|
||||
creteDefault(SymmetricKey.keyLength * numberOfKeys, this, salt)
|
||||
|
||||
/**
|
||||
* Derive multiple keys from the password. Derivation params will be included in the key ids, see
|
||||
@@ -58,13 +59,13 @@ sealed class KDF {
|
||||
* to change with time.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
fun deriveMultiple(password: String, count: Int): List<SymmetricKey> =
|
||||
kdfForSize(count).deriveMultipleKeys(password, count)
|
||||
fun deriveMultiple(password: String, count: Int,salt: UByteArray): List<SymmetricKey> =
|
||||
kdfForSize(count, salt).deriveMultipleKeys(password, count)
|
||||
|
||||
/**
|
||||
* Derive single key from password, same as [deriveMultiple] with count=1.
|
||||
*/
|
||||
fun derive(password: String): SymmetricKey = deriveMultiple(password, 1).first()
|
||||
fun derive(password: String, salt: UByteArray): SymmetricKey = deriveMultiple(password, 1, salt).first()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
package net.sergeych.utools
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
fun now(): Instant = Clock.System.now()
|
||||
fun nowToSeconds(): Instant = Clock.System.now().truncateToSeconds()
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -10,25 +10,17 @@
|
||||
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.time.Clock
|
||||
import net.sergeych.crypto2.Hash
|
||||
import net.sergeych.crypto2.initCrypto
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextUBytes
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
|
||||
@Suppress("UNUSED_PARAMETER", "UNUSED_VARIABLE")
|
||||
suspend fun <T> sw(label: String, f: suspend () -> T): T {
|
||||
val t1 = Clock.System.now()
|
||||
val result = f()
|
||||
val t2 = Clock.System.now()
|
||||
// println("$label: ${t2 - t1}")
|
||||
return result
|
||||
}
|
||||
|
||||
class HashTest {
|
||||
@Test
|
||||
fun testEqualMethods() {
|
||||
@@ -77,3 +69,13 @@ class HashTest {
|
||||
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER", "UNUSED_VARIABLE")
|
||||
suspend fun <T> sw(label: String, f: suspend () -> T): T {
|
||||
val t1 = Clock.System.now()
|
||||
val result = f()
|
||||
val t2 = Clock.System.now()
|
||||
// println("$label: ${t2 - t1}")
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -442,7 +442,7 @@ class KeysTest {
|
||||
assertContentEquals(k2.keyBytes, k2.id.id.body)
|
||||
|
||||
val k7 = SymmetricKey.new()
|
||||
val k8 = KDF.Complexity.Interactive.derive("super")
|
||||
val k8 = KDF.Complexity.Interactive.derive("super", KDF.Argon.randomSalt())
|
||||
|
||||
fun testToString(k: UniversalKey) {
|
||||
val s = k.toString()
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Instant
|
||||
import net.sergeych.crypto2.initCrypto
|
||||
import net.sergeych.utools.nowToSeconds
|
||||
import net.sergeych.utools.pack
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.bintools.*
|
||||
import net.sergeych.bipack.decodeFromBipack
|
||||
import net.sergeych.crypto2.DecryptionFailedException
|
||||
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.assertFailsWith
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class StorageTest {
|
||||
|
||||
@Test
|
||||
fun testGetAndSet() = runTest {
|
||||
initCrypto()
|
||||
val plain = MemoryKVStorage()
|
||||
val key = SymmetricKey.new()
|
||||
val storage = EncryptedKVStorage(plain, key, removeExisting = false)
|
||||
|
||||
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, removeExisting = false)
|
||||
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)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeleteExisting() = runTest {
|
||||
initCrypto()
|
||||
val plain = MemoryKVStorage()
|
||||
val c1 = EncryptedKVStorage(plain, SymmetricKey.new(), removeExisting = false) // 1
|
||||
c1.write("hello", "world")
|
||||
assertFailsWith<DecryptionFailedException> {
|
||||
EncryptedKVStorage(plain, SymmetricKey.new(), removeExisting = false) // 2
|
||||
}
|
||||
EncryptedKVStorage(plain, SymmetricKey.new(), removeExisting = true) // 2
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun KVStorage.dump() {
|
||||
for (k in keys)
|
||||
println("$k: ${this[k]?.toDump()}")
|
||||
}
|
||||
Reference in new issue
Block a user