Compare commits
No commits in common. "master" and "master" have entirely different histories.
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,7 +9,6 @@ build/
|
|||||||
.idea/jarRepositories.xml
|
.idea/jarRepositories.xml
|
||||||
.idea/compiler.xml
|
.idea/compiler.xml
|
||||||
.idea/libraries/
|
.idea/libraries/
|
||||||
.idea
|
|
||||||
*.iws
|
*.iws
|
||||||
*.iml
|
*.iml
|
||||||
*.ipr
|
*.ipr
|
||||||
|
|||||||
7
.idea/.gitignore
generated
vendored
7
.idea/.gitignore
generated
vendored
@ -1,3 +1,10 @@
|
|||||||
# Default ignored files
|
# Default ignored files
|
||||||
/shelf/
|
/shelf/
|
||||||
/workspace.xml
|
/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
.idea/artifacts/crypto2_js_0_1_1_SNAPSHOT.xml
generated
Normal file
6
.idea/artifacts/crypto2_js_0_1_1_SNAPSHOT.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<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
.idea/artifacts/crypto2_js_1_0_SNAPSHOT.xml
generated
Normal file
8
.idea/artifacts/crypto2_js_1_0_SNAPSHOT.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<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
.idea/artifacts/crypto2_jvm_0_1_1_SNAPSHOT.xml
generated
Normal file
6
.idea/artifacts/crypto2_jvm_0_1_1_SNAPSHOT.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<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
.idea/artifacts/crypto2_jvm_1_0_SNAPSHOT.xml
generated
Normal file
8
.idea/artifacts/crypto2_jvm_1_0_SNAPSHOT.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<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>
|
||||||
8
.idea/artifacts/crypto2_wasm_js_0_1_1_SNAPSHOT.xml
generated
Normal file
8
.idea/artifacts/crypto2_wasm_js_0_1_1_SNAPSHOT.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<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>
|
||||||
24
.idea/codeStyles/Project.xml
generated
24
.idea/codeStyles/Project.xml
generated
@ -1,29 +1,5 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<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>
|
<ScalaCodeStyleSettings>
|
||||||
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
|
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
|
||||||
</ScalaCodeStyleSettings>
|
</ScalaCodeStyleSettings>
|
||||||
|
|||||||
2
.idea/gradle.xml
generated
2
.idea/gradle.xml
generated
@ -5,7 +5,6 @@
|
|||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleHome" value="/usr/local/Cellar/gradle/7.6/libexec" />
|
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
@ -13,6 +12,5 @@
|
|||||||
</option>
|
</option>
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
<option name="parallelModelFetch" value="true" />
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<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>
|
||||||
6
.idea/kotlinc.xml
generated
Normal file
6
.idea/kotlinc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="1.9.20" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -3,7 +3,7 @@
|
|||||||
<component name="FrameworkDetectionExcludesConfiguration">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
<file type="web" url="file://$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="corretto-17" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17 (5)" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
16
README.md
16
README.md
@ -2,16 +2,6 @@
|
|||||||
|
|
||||||
Kotlin Multiplatform cryptographic primitives using modern strong cryptography.
|
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.
|
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.
|
All primitives meant to send over the network or store are `kotlinx.serialization` compatible, serializers included.
|
||||||
@ -31,7 +21,7 @@ repositories {
|
|||||||
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
import("net.sergeych:crypto2:0.8.4")
|
import("net.sergeych:crypto2:0.7.1-SNAPSHOT")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -110,10 +100,6 @@ Keys could be associated with tags. Keyrings are used primarily to store keys in
|
|||||||
|
|
||||||
Using very strong Argon_v2id, and adjustable complexity. Allows storing password key derivation parameters (included in the generated symmetric keys) to re-derive keys later, allows multiple keys derivation. All structures meant to be stored are serializable.
|
Using very strong Argon_v2id, and adjustable complexity. Allows storing password key derivation parameters (included in the generated symmetric keys) to re-derive keys later, allows multiple keys derivation. All structures meant to be stored are serializable.
|
||||||
|
|
||||||
KDF convenience constructors accept salt bytes directly. When the provided salt is not exactly the Argon salt size, it is deterministically normalized with `Hash.Blake2b.deriveSaltFormBytes(...)` before the KDF is created; exact-size salts are used as is.
|
|
||||||
|
|
||||||
`KDF.deriveFromBytes(source)` derives exactly the configured KDF key size. Create the KDF with the required key size first, for example through `KDF.createDefault(...)` or `Complexity.kdfForSizeInBytes(...)`.
|
|
||||||
|
|
||||||
## Unified keys hierarchy
|
## Unified keys hierarchy
|
||||||
|
|
||||||
Allows the application code to use proper key abstraction and work with more key types in the future, e.g. `SigningKey`, `VerifyingKey`, `EncryptingKey` and `DecryptingKey`. Effective key generation and random byte sequence producers.
|
Allows the application code to use proper key abstraction and work with more key types in the future, e.g. `SigningKey`, `VerifyingKey`, `EncryptingKey` and `DecryptingKey`. Effective key generation and random byte sequence producers.
|
||||||
|
|||||||
@ -13,14 +13,14 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform") version "2.2.21"
|
kotlin("multiplatform") version "2.0.21"
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.21"
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.21"
|
||||||
id("org.jetbrains.dokka") version "1.9.20"
|
id("org.jetbrains.dokka") version "1.9.20"
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "0.9.3"
|
version = "0.8.3-SNAPSHOT"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@ -31,8 +31,12 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(21)
|
jvm {
|
||||||
jvm()
|
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = JvmTarget.JVM_11
|
||||||
|
}
|
||||||
|
}
|
||||||
js {
|
js {
|
||||||
browser()
|
browser()
|
||||||
nodejs()
|
nodejs()
|
||||||
@ -40,12 +44,12 @@ kotlin {
|
|||||||
linuxX64()
|
linuxX64()
|
||||||
linuxArm64()
|
linuxArm64()
|
||||||
|
|
||||||
macosX64()
|
// macosX64()
|
||||||
macosArm64()
|
// macosArm64()
|
||||||
iosX64()
|
// iosX64()
|
||||||
iosArm64()
|
// iosArm64()
|
||||||
iosSimulatorArm64()
|
// iosSimulatorArm64()
|
||||||
mingwX64()
|
// mingwX64()
|
||||||
@OptIn(ExperimentalWasmDsl::class)
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
wasmJs {
|
wasmJs {
|
||||||
browser()
|
browser()
|
||||||
@ -56,27 +60,26 @@ kotlin {
|
|||||||
languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi")
|
languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi")
|
||||||
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
|
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||||
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
|
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
|
||||||
languageSettings.optIn("kotlin.time.ExperimentalTime")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
|
||||||
|
|
||||||
implementation("net.sergeych:multiplatform-crypto-libsodium-bindings:0.9.6")
|
implementation("net.sergeych:multiplatform-crypto-libsodium-bindings:0.9.6")
|
||||||
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.10")
|
api("com.ionspin.kotlin:bignum:0.3.9")
|
||||||
api("net.sergeych:mp_bintools:0.3.2")
|
api("net.sergeych:mp_bintools:0.1.12-SNAPSHOT")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val commonTest by getting {
|
val commonTest by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("test"))
|
implementation(kotlin("test"))
|
||||||
implementation("org.slf4j:slf4j-simple:2.0.9")
|
implementation("org.slf4j:slf4j-simple:2.0.9")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val native by creating {
|
val native by creating {
|
||||||
@ -112,14 +115,6 @@ 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 {
|
tasks.dokkaHtml.configure {
|
||||||
outputDirectory.set(buildDir.resolve("dokka"))
|
outputDirectory.set(buildDir.resolve("dokka"))
|
||||||
dokkaSourceSets {
|
dokkaSourceSets {
|
||||||
|
|||||||
@ -9,7 +9,3 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
kotlin.code.style=official
|
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
|
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -10,6 +10,6 @@
|
|||||||
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
@ -321,27 +321,6 @@ sealed class Container {
|
|||||||
}.build()
|
}.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 {
|
companion object {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,78 +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
|
|
||||||
|
|
||||||
internal val Hash.supportsHmac: Boolean
|
|
||||||
get() = when (this) {
|
|
||||||
Hash.Blake2b, Hash.Sha3_256, Hash.Sha3_384 -> true
|
|
||||||
Hash.Sha3AndBlake -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val Hash.hkdfMaxOutputSize: Int
|
|
||||||
get() = hkdfHashSize * 255
|
|
||||||
|
|
||||||
private val Hash.hkdfHashSize: Int
|
|
||||||
get() = digest(ubyteArrayOf()).size
|
|
||||||
|
|
||||||
private val Hash.hmacBlockSize: Int
|
|
||||||
get() = when (this) {
|
|
||||||
Hash.Blake2b -> 128
|
|
||||||
Hash.Sha3_256 -> 136
|
|
||||||
Hash.Sha3_384 -> 104
|
|
||||||
Hash.Sha3AndBlake -> throw IllegalArgumentException("Hash $this is not supported for HMAC")
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun hkdf(
|
|
||||||
hash: Hash,
|
|
||||||
source: UByteArray,
|
|
||||||
salt: UByteArray,
|
|
||||||
info: UByteArray,
|
|
||||||
size: Int,
|
|
||||||
): UByteArray {
|
|
||||||
require(source.isNotEmpty()) { "HKDF source bytes should not be empty" }
|
|
||||||
require(size > 0) { "HKDF output size should be positive" }
|
|
||||||
require(hash.supportsHmac) { "Hash $hash is not supported for HKDF" }
|
|
||||||
require(size <= hash.hkdfMaxOutputSize) {
|
|
||||||
"HKDF output for $hash is limited to ${hash.hkdfMaxOutputSize} bytes"
|
|
||||||
}
|
|
||||||
|
|
||||||
val hashSize = hash.hkdfHashSize
|
|
||||||
val extractionSalt = if (salt.isEmpty()) UByteArray(hashSize) else salt
|
|
||||||
val pseudorandomKey = hmac(hash, extractionSalt, source)
|
|
||||||
val result = UByteArray(size)
|
|
||||||
var previous = ubyteArrayOf()
|
|
||||||
var offset = 0
|
|
||||||
var counter = 1
|
|
||||||
|
|
||||||
while (offset < size) {
|
|
||||||
previous = hmac(hash, pseudorandomKey, previous + info + counter.toUByte())
|
|
||||||
val count = minOf(previous.size, size - offset)
|
|
||||||
for (i in 0..<count) {
|
|
||||||
result[offset + i] = previous[i]
|
|
||||||
}
|
|
||||||
offset += count
|
|
||||||
counter += 1
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hmac(hash: Hash, key: UByteArray, data: UByteArray): UByteArray {
|
|
||||||
val blockSize = hash.hmacBlockSize
|
|
||||||
val normalizedKey = if (key.size > blockSize) hash.digest(key) else key
|
|
||||||
val keyBlock = UByteArray(blockSize)
|
|
||||||
for (i in normalizedKey.indices) {
|
|
||||||
keyBlock[i] = normalizedKey[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
val innerPad = UByteArray(blockSize) { (keyBlock[it].toInt() xor 0x36).toUByte() }
|
|
||||||
val outerPad = UByteArray(blockSize) { (keyBlock[it].toInt() xor 0x5c).toUByte() }
|
|
||||||
return hash.digest(outerPad + hash.digest(innerPad + data))
|
|
||||||
}
|
|
||||||
@ -129,41 +129,27 @@ enum class Hash(
|
|||||||
* strong to brute force, but it is good when you just need to get a deterministic salt of arbitrary size.
|
* strong to brute force, but it is good when you just need to get a deterministic salt of arbitrary size.
|
||||||
*
|
*
|
||||||
* _Note that deriving salt of sizes less than hash block will reduce hash strength and should not be allowed
|
* _Note that deriving salt of sizes less than hash block will reduce hash strength and should not be allowed
|
||||||
* in a situation where strength is of concern while extending its length above hash block size does not improve
|
* in situation where strength is of concern while extending its length above hash block size does not improve
|
||||||
* it_. The only reason to use this function is when the desired _salt size_ is not equal to block size, or
|
* it_. The only reason to use this function is when the desired _salt size_ is not equal to block size, or
|
||||||
* not known beforehand.
|
* not known beforehand.1
|
||||||
*
|
*
|
||||||
* To get a cryptographically safe (more or less) key from password use [KDF] classes, or [KDF.deriveKey]
|
* To get a cryptographically safe (more or less) key from password use [KDF] classes, or [KDF.deriveKey]
|
||||||
* and [KDF.deriveMultipleKeys].
|
* and [KDF.deriveMultipleKeys].
|
||||||
*/
|
*/
|
||||||
fun deriveSalt(base: String, sizeInBytes: Int): UByteArray =
|
fun deriveSalt(base: String, sizeInBytes: Int): UByteArray {
|
||||||
deriveSaltFromBytes(base.encodeToUByteArray(), sizeInBytes)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Derive a salt of any size from binary data.
|
|
||||||
*
|
|
||||||
* @see deriveSalt
|
|
||||||
*/
|
|
||||||
fun deriveSaltFromBytes(base: UByteArray, sizeInBytes: Int): UByteArray {
|
|
||||||
require(sizeInBytes > 0)
|
require(sizeInBytes > 0)
|
||||||
val result = mutableListOf<UByte>()
|
val result = mutableListOf<UByte>()
|
||||||
var round = 0
|
var round = 0
|
||||||
var src = base
|
var src = base.encodeToUByteArray()
|
||||||
do {
|
do {
|
||||||
src = "rnd_${round++}_".encodeToUByteArray() + src
|
src = "rnd_${round++}_".encodeToUByteArray() + src
|
||||||
val hash = digest(src)
|
val hash = digest(src)
|
||||||
if (result.size + hash.size <= sizeInBytes) result += hash
|
if (result.size + hash.size <= sizeInBytes) result += hash
|
||||||
else result += hash.copyOf(sizeInBytes - result.size)
|
else result += hash.slice(0..<(sizeInBytes - result.size))
|
||||||
} while (result.size < sizeInBytes)
|
} while (result.size < sizeInBytes)
|
||||||
return result.toUByteArray()
|
return result.toUByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Typo-compatible alias for [deriveSaltFromBytes].
|
|
||||||
*/
|
|
||||||
fun deriveSaltFormBytes(base: UByteArray, sizeInBytes: Int): UByteArray =
|
|
||||||
deriveSaltFromBytes(base, sizeInBytes)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,65 +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 com.ionspin.kotlin.crypto.generichash.GenericHash
|
|
||||||
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deterministic seed derivation helper.
|
|
||||||
*
|
|
||||||
* It compresses arbitrary byte material to an exact seed size using domain-separated BLAKE2b.
|
|
||||||
* This is intended for already strong random byte material. Do not use it as a password KDF;
|
|
||||||
* use [KDF] and [PBKD] for passwords and other low-entropy secrets.
|
|
||||||
*/
|
|
||||||
object KeySeed {
|
|
||||||
private const val maxBlake2bOutput = 64
|
|
||||||
private const val minBlake2bOutput = 16
|
|
||||||
private val apiKey = "net.sergeych.crypto2.KeySeed.v1".encodeToUByteArray()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Derive exactly [size] bytes suitable for APIs expecting fixed-size cryptographic seeds.
|
|
||||||
*
|
|
||||||
* [purpose] domain-separates seeds for different key types. Use a stable, key-specific value
|
|
||||||
* when deriving more than one seed from the same [source].
|
|
||||||
*/
|
|
||||||
fun fromBytes(source: UByteArray, size: Int, purpose: String = "default"): UByteArray {
|
|
||||||
require(size > 0) { "seed size should be positive" }
|
|
||||||
require(source.isNotEmpty()) { "source bytes should not be empty" }
|
|
||||||
val key = GenericHash.genericHash(apiKey + purpose.encodeToUByteArray(), 32)
|
|
||||||
if (size <= maxBlake2bOutput) {
|
|
||||||
val hashSize = size.coerceAtLeast(minBlake2bOutput)
|
|
||||||
return GenericHash.genericHash(source, hashSize, key).copyOf(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = mutableListOf<UByte>()
|
|
||||||
var round = 0
|
|
||||||
while (result.size < size) {
|
|
||||||
val block = GenericHash.genericHash(
|
|
||||||
round.toLittleEndianBytes() + size.toLittleEndianBytes() + source,
|
|
||||||
maxBlake2bOutput,
|
|
||||||
key
|
|
||||||
)
|
|
||||||
val count = minOf(block.size, size - result.size)
|
|
||||||
result += block.take(count)
|
|
||||||
round += 1
|
|
||||||
}
|
|
||||||
return result.toUByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Int.toLittleEndianBytes(): UByteArray =
|
|
||||||
ubyteArrayOf(
|
|
||||||
(this and 0xff).toUByte(),
|
|
||||||
((this ushr 8) and 0xff).toUByte(),
|
|
||||||
((this ushr 16) and 0xff).toUByte(),
|
|
||||||
((this ushr 24) and 0xff).toUByte(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -60,12 +60,6 @@ sealed class Multikey {
|
|||||||
*/
|
*/
|
||||||
abstract fun check(keys: Iterable<VerifyingPublicKey>): Boolean
|
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
|
* Check that [verifyingKeys] satisfy the multikey condition
|
||||||
*/
|
*/
|
||||||
@ -103,10 +97,6 @@ sealed class Multikey {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mentionedKeys(): Set<VerifyingPublicKey> {
|
|
||||||
return validKeys
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,10 +119,6 @@ sealed class Multikey {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mentionedKeys(): Set<VerifyingPublicKey> {
|
|
||||||
return validKeys.flatMap { it.mentionedKeys() }.toSet()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -143,7 +129,6 @@ sealed class Multikey {
|
|||||||
@Serializable
|
@Serializable
|
||||||
object AnyKey : Multikey() {
|
object AnyKey : Multikey() {
|
||||||
override fun check(keys: Iterable<VerifyingPublicKey>): Boolean = true
|
override fun check(keys: Iterable<VerifyingPublicKey>): Boolean = true
|
||||||
override fun mentionedKeys(): Set<VerifyingPublicKey> = emptySet()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -11,14 +11,7 @@
|
|||||||
package net.sergeych.crypto2
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
|
import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
|
||||||
import com.ionspin.kotlin.crypto.keyexchange.KeyExchangeKeyPair
|
|
||||||
import com.ionspin.kotlin.crypto.keyexchange.crypto_kx_PUBLICKEYBYTES
|
|
||||||
import com.ionspin.kotlin.crypto.keyexchange.crypto_kx_SECRETKEYBYTES
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import net.sergeych.crypto2.SafeKeyExchange.SessionKey
|
import net.sergeych.crypto2.SafeKeyExchange.SessionKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,7 +20,7 @@ import net.sergeych.crypto2.SafeKeyExchange.SessionKey
|
|||||||
* Usage:
|
* Usage:
|
||||||
*
|
*
|
||||||
* 1. Create [SafeKeyExchange] on both server and client sides
|
* 1. Create [SafeKeyExchange] on both server and client sides
|
||||||
* 2. Exchange [PublicKey] instances
|
* 2. Exchange [EncryptingPublicKey] instances
|
||||||
* 3. Create [serverSessionKey] and [clientSessionKey] respectively
|
* 3. Create [serverSessionKey] and [clientSessionKey] respectively
|
||||||
* 4. Use [SessionKey.sendingKey] and [SessionKey.receivingKey] to send and receive encrypted data.
|
* 4. Use [SessionKey.sendingKey] and [SessionKey.receivingKey] to send and receive encrypted data.
|
||||||
*
|
*
|
||||||
@ -38,44 +31,9 @@ import net.sergeych.crypto2.SafeKeyExchange.SessionKey
|
|||||||
* - while it is possible to generate several keys "ahead", the care should be taken when storing them,
|
* - while it is possible to generate several keys "ahead", the care should be taken when storing them,
|
||||||
* encrypt it with some other key to maintain safety.
|
* encrypt it with some other key to maintain safety.
|
||||||
* - do not use [EncryptingPublicKey] for anything but creating session keys.
|
* - do not use [EncryptingPublicKey] for anything but creating session keys.
|
||||||
* - the serialized form includes the secret exchange key, so it should be encrypted when stored.
|
|
||||||
*/
|
*/
|
||||||
@Serializable(with = SafeKeyExchange.SafeKeyExchangeSerializer::class)
|
class SafeKeyExchange {
|
||||||
class SafeKeyExchange private constructor(
|
private val pair = KeyExchange.keypair()
|
||||||
private val pair: KeyExchangeKeyPair,
|
|
||||||
) {
|
|
||||||
|
|
||||||
constructor() : this(KeyExchange.keypair())
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private class Packed(
|
|
||||||
val publicKey: UByteArray,
|
|
||||||
val secretKey: UByteArray,
|
|
||||||
)
|
|
||||||
|
|
||||||
object SafeKeyExchangeSerializer : KSerializer<SafeKeyExchange> {
|
|
||||||
private val packedSerializer = Packed.serializer()
|
|
||||||
|
|
||||||
override val descriptor: SerialDescriptor = packedSerializer.descriptor
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: SafeKeyExchange) {
|
|
||||||
encoder.encodeSerializableValue(
|
|
||||||
packedSerializer,
|
|
||||||
Packed(value.pair.publicKey, value.pair.secretKey)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): SafeKeyExchange {
|
|
||||||
val packed = decoder.decodeSerializableValue(packedSerializer)
|
|
||||||
require(packed.publicKey.size == crypto_kx_PUBLICKEYBYTES) {
|
|
||||||
"SafeKeyExchange public key must be $crypto_kx_PUBLICKEYBYTES bytes"
|
|
||||||
}
|
|
||||||
require(packed.secretKey.size == crypto_kx_SECRETKEYBYTES) {
|
|
||||||
"SafeKeyExchange secret key must be $crypto_kx_SECRETKEYBYTES bytes"
|
|
||||||
}
|
|
||||||
return SafeKeyExchange(KeyExchangeKeyPair(packed.publicKey, packed.secretKey))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The session key. It uses a pair of keys to encrypt and decrypt messages to maintain high
|
* The session key. It uses a pair of keys to encrypt and decrypt messages to maintain high
|
||||||
@ -132,24 +90,9 @@ class SafeKeyExchange private constructor(
|
|||||||
* The public key; it should be transmitted to the other party, this is serializable.
|
* The public key; it should be transmitted to the other party, this is serializable.
|
||||||
* Do not use it except to get [SessionKey] with [clientSessionKey] or [serverSessionKey]. Storing and reusing
|
* Do not use it except to get [SessionKey] with [clientSessionKey] or [serverSessionKey]. Storing and reusing
|
||||||
* it is a great danger.
|
* it is a great danger.
|
||||||
*
|
|
||||||
* Instances can be compared and used as hashtable keys.
|
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
class PublicKey(val keyBytes: UByteArray) {
|
class PublicKey(val keyBytes: UByteArray)
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other == null || this::class != other::class) return false
|
|
||||||
|
|
||||||
other as PublicKey
|
|
||||||
|
|
||||||
return keyBytes.contentEquals(other.keyBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return keyBytes.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The public key to be sent to the other party. When received, get the session keys with [clientSessionKey]
|
* The public key to be sent to the other party. When received, get the session keys with [clientSessionKey]
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
package net.sergeych.crypto2
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
import kotlin.time.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import net.sergeych.bipack.BipackEncoder
|
import net.sergeych.bipack.BipackEncoder
|
||||||
import net.sergeych.bipack.decodeFromBipack
|
import net.sergeych.bipack.decodeFromBipack
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
package net.sergeych.crypto2
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
import kotlin.time.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import net.sergeych.bipack.BipackDecoder
|
import net.sergeych.bipack.BipackDecoder
|
||||||
|
|||||||
@ -11,7 +11,6 @@
|
|||||||
package net.sergeych.crypto2
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
import com.ionspin.kotlin.crypto.box.Box
|
import com.ionspin.kotlin.crypto.box.Box
|
||||||
import com.ionspin.kotlin.crypto.box.crypto_box_SEEDBYTES
|
|
||||||
import com.ionspin.kotlin.crypto.scalarmult.ScalarMultiplication
|
import com.ionspin.kotlin.crypto.scalarmult.ScalarMultiplication
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@ -88,11 +87,6 @@ class DecryptingSecretKey(
|
|||||||
companion object {
|
companion object {
|
||||||
data class KeyPair(val secretKey: DecryptingSecretKey, val publicKey: EncryptingPublicKey)
|
data class KeyPair(val secretKey: DecryptingSecretKey, val publicKey: EncryptingPublicKey)
|
||||||
|
|
||||||
/**
|
|
||||||
* Required seed size for deterministic decrypting key generation.
|
|
||||||
*/
|
|
||||||
val seedLength = crypto_box_SEEDBYTES
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a new random pair of public and secret keys.
|
* Generate a new random pair of public and secret keys.
|
||||||
*/
|
*/
|
||||||
@ -102,30 +96,7 @@ class DecryptingSecretKey(
|
|||||||
return KeyPair(DecryptingSecretKey(p.secretKey, pk), pk)
|
return KeyPair(DecryptingSecretKey(p.secretKey, pk), pk)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the key pair deterministically from an exact [seedLength]-byte seed.
|
|
||||||
*/
|
|
||||||
fun generateKeys(seed: UByteArray): KeyPair {
|
|
||||||
require(seed.size == seedLength) { "seed should be $seedLength bytes" }
|
|
||||||
val p = Box.seedKeypair(seed)
|
|
||||||
val pk = EncryptingPublicKey(p.publicKey)
|
|
||||||
return KeyPair(DecryptingSecretKey(p.secretKey, pk), pk)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun new(): DecryptingSecretKey = generateKeys().secretKey
|
fun new(): DecryptingSecretKey = generateKeys().secretKey
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a deterministic secret key from an exact [seedLength]-byte seed.
|
|
||||||
*/
|
|
||||||
fun fromSeed(seed: UByteArray): DecryptingSecretKey = generateKeys(seed).secretKey
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Derive an exact [seedLength]-byte seed from arbitrary byte material.
|
|
||||||
*
|
|
||||||
* Use this only with high-entropy source bytes. It is not a password KDF.
|
|
||||||
*/
|
|
||||||
fun seedFromBytes(source: UByteArray): UByteArray =
|
|
||||||
KeySeed.fromBytes(source, seedLength, "DecryptingSecretKey")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
package net.sergeych.crypto2
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
import kotlin.time.Instant
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
interface SigningKey: KeyInstance {
|
interface SigningKey: KeyInstance {
|
||||||
val verifyingKey: VerifyingPublicKey
|
val verifyingKey: VerifyingPublicKey
|
||||||
|
|||||||
@ -11,8 +11,7 @@
|
|||||||
package net.sergeych.crypto2
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
import com.ionspin.kotlin.crypto.signature.Signature
|
import com.ionspin.kotlin.crypto.signature.Signature
|
||||||
import com.ionspin.kotlin.crypto.signature.crypto_sign_SEEDBYTES
|
import kotlinx.datetime.Instant
|
||||||
import kotlin.time.Instant
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
@ -77,41 +76,13 @@ class SigningSecretKey(
|
|||||||
|
|
||||||
data class SigningKeyPair(val secretKey: SigningSecretKey, val publicKey: VerifyingPublicKey)
|
data class SigningKeyPair(val secretKey: SigningSecretKey, val publicKey: VerifyingPublicKey)
|
||||||
|
|
||||||
/**
|
|
||||||
* Required seed size for deterministic signing key generation.
|
|
||||||
*/
|
|
||||||
const val seedLength = crypto_sign_SEEDBYTES
|
|
||||||
|
|
||||||
fun generatePair(): SigningKeyPair {
|
fun generatePair(): SigningKeyPair {
|
||||||
val p = Signature.keypair()
|
val p = Signature.keypair()
|
||||||
val publicKey = VerifyingPublicKey(p.publicKey)
|
val publicKey = VerifyingPublicKey(p.publicKey)
|
||||||
return SigningKeyPair(SigningSecretKey(p.secretKey, publicKey), publicKey)
|
return SigningKeyPair(SigningSecretKey(p.secretKey, publicKey), publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the key pair deterministically from an exact [seedLength]-byte seed.
|
|
||||||
*/
|
|
||||||
fun generatePair(seed: UByteArray): SigningKeyPair {
|
|
||||||
require(seed.size == seedLength) { "seed should be $seedLength bytes" }
|
|
||||||
val p = Signature.seedKeypair(seed)
|
|
||||||
val publicKey = VerifyingPublicKey(p.publicKey)
|
|
||||||
return SigningKeyPair(SigningSecretKey(p.secretKey, publicKey), publicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun new(): SigningSecretKey = generatePair().secretKey
|
fun new(): SigningSecretKey = generatePair().secretKey
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a deterministic signing key from an exact [seedLength]-byte seed.
|
|
||||||
*/
|
|
||||||
fun fromSeed(seed: UByteArray): SigningSecretKey = generatePair(seed).secretKey
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Derive an exact [seedLength]-byte seed from arbitrary byte material.
|
|
||||||
*
|
|
||||||
* Use this only with high-entropy source bytes. It is not a password KDF.
|
|
||||||
*/
|
|
||||||
fun seedFromBytes(source: UByteArray): UByteArray =
|
|
||||||
KeySeed.fromBytes(source, seedLength, "SigningSecretKey")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,54 +46,13 @@ class SymmetricKey(
|
|||||||
data class WithNonce(
|
data class WithNonce(
|
||||||
val cipherData: UByteArray,
|
val cipherData: UByteArray,
|
||||||
val nonce: UByteArray,
|
val nonce: UByteArray,
|
||||||
) {
|
)
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other == null || this::class != other::class) return false
|
|
||||||
|
|
||||||
other as WithNonce
|
|
||||||
|
|
||||||
if (!cipherData.contentEquals(other.cipherData)) return false
|
|
||||||
if (!nonce.contentEquals(other.nonce)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = cipherData.contentHashCode()
|
|
||||||
result = 31 * result + nonce.contentHashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun encryptWithNonce(plainData: UByteArray, nonce: UByteArray, randomFill: IntRange?): UByteArray {
|
override fun encryptWithNonce(plainData: UByteArray, nonce: UByteArray, randomFill: IntRange?): UByteArray {
|
||||||
require(nonce.size == nonceLength)
|
require(nonce.size == nonceLength)
|
||||||
return SecretBox.easy(WithFill.encode(plainData, randomFill), nonce, keyBytes)
|
return SecretBox.easy(WithFill.encode(plainData, randomFill), nonce, keyBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Compact authenticated encryption with a caller-provided nonce.
|
|
||||||
*
|
|
||||||
* This method is meant for storage formats where the nonce is derived or stored elsewhere
|
|
||||||
* and every byte of encoded output matters, for example short encrypted file names.
|
|
||||||
* The output is the raw SecretBox ciphertext only: [plainData] size plus 16 authentication
|
|
||||||
* bytes. The nonce is not stored in the output and must be reproduced exactly for
|
|
||||||
* [decryptCompactWithNonce].
|
|
||||||
*
|
|
||||||
* Unlike [encryptWithNonce], this method does not wrap [plainData] with [WithFill]. For
|
|
||||||
* short payloads this usually saves only 1 byte over [encryptWithNonce], because both
|
|
||||||
* methods already require the caller to manage the nonce separately. Use it only when this
|
|
||||||
* small size gain is important or when the surrounding format explicitly requires raw
|
|
||||||
* SecretBox output.
|
|
||||||
*
|
|
||||||
* Never reuse the same nonce with the same key for different plaintext. Reusing a
|
|
||||||
* `(key, nonce)` pair with SecretBox breaks the security of the stream cipher.
|
|
||||||
*/
|
|
||||||
fun encryptCompactWithNonce(plainData: UByteArray, nonce: UByteArray): UByteArray {
|
|
||||||
require(nonce.size == nonceLength)
|
|
||||||
return SecretBox.easy(plainData, nonce, keyBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val nonceBytesLength: Int = nonceLength
|
override val nonceBytesLength: Int = nonceLength
|
||||||
|
|
||||||
override val id by lazy {
|
override val id by lazy {
|
||||||
@ -105,22 +64,6 @@ class SymmetricKey(
|
|||||||
WithFill.decode(SecretBox.openEasy(cipherData, nonce, keyBytes))
|
WithFill.decode(SecretBox.openEasy(cipherData, nonce, keyBytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Compact authenticated decryption with a caller-provided nonce.
|
|
||||||
*
|
|
||||||
* Decrypts data produced by [encryptCompactWithNonce]. The nonce is not read from
|
|
||||||
* [cipherData] and must be the exact same nonce that was used for encryption. Use this
|
|
||||||
* method only for compact formats that intentionally omit both the nonce and the [WithFill]
|
|
||||||
* wrapper.
|
|
||||||
*
|
|
||||||
* @throws DecryptionFailedException if the key, nonce, or cipher data is invalid.
|
|
||||||
*/
|
|
||||||
fun decryptCompactWithNonce(cipherData: UByteArray, nonce: UByteArray): UByteArray =
|
|
||||||
protectDecryption {
|
|
||||||
require(nonce.size == nonceLength)
|
|
||||||
SecretBox.openEasy(cipherData, nonce, keyBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is SymmetricKey) return false
|
if (other !is SymmetricKey) return false
|
||||||
|
|||||||
@ -48,38 +48,10 @@ class UniversalPrivateKey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
|
||||||
* Required seed size for deterministic universal private key generation.
|
|
||||||
*
|
|
||||||
* It contains independent signing and decrypting seeds.
|
|
||||||
*/
|
|
||||||
val seedLength = SigningSecretKey.seedLength + DecryptingSecretKey.seedLength
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate 2 new random keys (4 key pairs under the hood) to securely signd and
|
* Generate 2 new random keys (4 key pairs under the hood) to securely signd and
|
||||||
* decrypt data.
|
* decrypt data.
|
||||||
*/
|
*/
|
||||||
fun new() = UniversalPrivateKey(SigningSecretKey.new(), DecryptingSecretKey.new())
|
fun new() = UniversalPrivateKey(SigningSecretKey.new(), DecryptingSecretKey.new())
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a deterministic private key from an exact [seedLength]-byte seed.
|
|
||||||
*/
|
|
||||||
fun fromSeed(seed: UByteArray): UniversalPrivateKey {
|
|
||||||
require(seed.size == seedLength) { "seed should be $seedLength bytes" }
|
|
||||||
val signingSeed = seed.sliceArray(0..<SigningSecretKey.seedLength)
|
|
||||||
val decryptingSeed = seed.sliceArray(SigningSecretKey.seedLength..<seedLength)
|
|
||||||
return UniversalPrivateKey(
|
|
||||||
SigningSecretKey.fromSeed(signingSeed),
|
|
||||||
DecryptingSecretKey.fromSeed(decryptingSeed)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Derive an exact [seedLength]-byte seed from arbitrary byte material.
|
|
||||||
*
|
|
||||||
* Use this only with high-entropy source bytes. It is not a password KDF.
|
|
||||||
*/
|
|
||||||
fun seedFromBytes(source: UByteArray): UByteArray =
|
|
||||||
KeySeed.fromBytes(source, seedLength, "UniversalPrivateKey")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -229,8 +229,6 @@ class UniversalRing(
|
|||||||
companion object {
|
companion object {
|
||||||
val EMPTY = UniversalRing(keyWithTags = emptyMap())
|
val EMPTY = UniversalRing(keyWithTags = emptyMap())
|
||||||
|
|
||||||
fun new(): UniversalRing = UniversalRing(keyWithTags = emptyMap())
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Join a collection of keyrings together (same as reducing with `+`). Correctly
|
* Join a collection of keyrings together (same as reducing with `+`). Correctly
|
||||||
* works if there is no keyring (returns [EMPTY]), or only one keyring (returns
|
* works if there is no keyring (returns [EMPTY]), or only one keyring (returns
|
||||||
|
|||||||
@ -42,28 +42,16 @@ sealed class KDF {
|
|||||||
/**
|
/**
|
||||||
* Create [KDF] of the corresponding strength suitable to derive [numberOfKeys] symmetric keys.
|
* Create [KDF] of the corresponding strength suitable to derive [numberOfKeys] symmetric keys.
|
||||||
*
|
*
|
||||||
* Random salt of proper size is used by default. If a custom [salt] size is not [Argon.saltSize],
|
* Random salt of proper size is used
|
||||||
* it is deterministically normalized with `Hash.Blake2b.deriveSaltFormBytes(...)`.
|
|
||||||
*/
|
*/
|
||||||
fun kdfForSize(numberOfKeys: Int, salt: UByteArray = Argon.randomSalt()): KDF =
|
fun kdfForSize(numberOfKeys: Int,salt: UByteArray = Argon.randomSalt()): KDF =
|
||||||
createDefault(SymmetricKey.keyLength * numberOfKeys, this, salt)
|
creteDefault(SymmetricKey.keyLength * numberOfKeys, this, salt)
|
||||||
|
|
||||||
/**
|
|
||||||
* Create [KDF] for deriving [sizeInBytes] bytes using the provided [salt].
|
|
||||||
*
|
|
||||||
* The [salt] is used as is when its size is [Argon.saltSize]. Otherwise, it is deterministically
|
|
||||||
* normalized with `Hash.Blake2b.deriveSaltFormBytes(...)` to the required size.
|
|
||||||
*/
|
|
||||||
fun kdfForSizeInBytes(sizeInBytes: Int, salt: UByteArray): KDF =
|
|
||||||
createDefault(sizeInBytes, this, salt)
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derive multiple keys from the password. Derivation params will be included in the key ids, see
|
* Derive multiple keys from the password. Derivation params will be included in the key ids, see
|
||||||
* [SymmetricKey.id] as [KeyId.kdp].
|
* [SymmetricKey.id] as [KeyId.kdp].
|
||||||
|
|
||||||
* If the [salt] size is not [Argon.saltSize], it is deterministically normalized with
|
* Random salt of proper size is used
|
||||||
* `Hash.Blake2b.deriveSaltFormBytes(...)`.
|
|
||||||
*
|
*
|
||||||
* ___Important: symmetric keys do not save key ids___. _Container do it, so it is possible to re-derive
|
* ___Important: symmetric keys do not save key ids___. _Container do it, so it is possible to re-derive
|
||||||
* key to open the container, but in many cases you might need to save [KeyId.kdp] separately_.
|
* key to open the container, but in many cases you might need to save [KeyId.kdp] separately_.
|
||||||
@ -71,22 +59,13 @@ sealed class KDF {
|
|||||||
* to change with time.
|
* to change with time.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun deriveMultiple(password: String, count: Int, salt: UByteArray): List<SymmetricKey> =
|
fun deriveMultiple(password: String, count: Int,salt: UByteArray): List<SymmetricKey> =
|
||||||
kdfForSize(count, salt).deriveMultipleKeys(password, count)
|
kdfForSize(count, salt).deriveMultipleKeys(password, count)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derive single key from password, same as [deriveMultiple] with count=1.
|
* Derive single key from password, same as [deriveMultiple] with count=1.
|
||||||
*/
|
*/
|
||||||
fun derive(password: String, salt: UByteArray): SymmetricKey = deriveMultiple(password, 1, salt).first()
|
fun derive(password: String, salt: UByteArray): SymmetricKey = deriveMultiple(password, 1, salt).first()
|
||||||
|
|
||||||
/**
|
|
||||||
* Derive bytes from bytes using the default [Argon] KDF with this complexity.
|
|
||||||
*
|
|
||||||
* If the [salt] size is not [Argon.saltSize], it is deterministically normalized with
|
|
||||||
* `Hash.Blake2b.deriveSaltFormBytes(...)`.
|
|
||||||
*/
|
|
||||||
fun derive(source: UByteArray, salt: UByteArray, derivedSize: Int): UByteArray =
|
|
||||||
createDefault(derivedSize, this, salt).deriveFromBytes(source)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,11 +73,6 @@ sealed class KDF {
|
|||||||
*/
|
*/
|
||||||
abstract fun deriveKey(password: String): UByteArray
|
abstract fun deriveKey(password: String): UByteArray
|
||||||
|
|
||||||
/**
|
|
||||||
* Derive bytes from bytes using this KDF parameters. The derived byte count is this KDF's configured key size.
|
|
||||||
*/
|
|
||||||
abstract fun deriveFromBytes(source: UByteArray): UByteArray
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derive keys from lower part of bytes derived from the password. E.g., if generated size is longer than
|
* Derive keys from lower part of bytes derived from the password. E.g., if generated size is longer than
|
||||||
* required to create [count] keys, the first bytes will be used. It let use rest bytes for other purposes.
|
* required to create [count] keys, the first bytes will be used. It let use rest bytes for other purposes.
|
||||||
@ -157,17 +131,6 @@ sealed class KDF {
|
|||||||
override fun deriveKey(password: String): UByteArray =
|
override fun deriveKey(password: String): UByteArray =
|
||||||
PasswordHash.pwhash(keySize, password, salt, instructionsComplexity, memComplexity, algorithm.code)
|
PasswordHash.pwhash(keySize, password, salt, instructionsComplexity, memComplexity, algorithm.code)
|
||||||
|
|
||||||
override fun deriveFromBytes(source: UByteArray): UByteArray {
|
|
||||||
return PasswordHash.pwhash(
|
|
||||||
keySize,
|
|
||||||
source.encodeToBase64Url(),
|
|
||||||
salt,
|
|
||||||
instructionsComplexity,
|
|
||||||
memComplexity,
|
|
||||||
algorithm.code
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Argon) return false
|
if (other !is Argon) return false
|
||||||
@ -208,7 +171,7 @@ sealed class KDF {
|
|||||||
|
|
||||||
fun create(complexity: Complexity, salt: UByteArray, keySize: Int): Argon {
|
fun create(complexity: Complexity, salt: UByteArray, keySize: Int): Argon {
|
||||||
require(salt.size == saltSize) { "The salt size should be $saltSize" }
|
require(salt.size == saltSize) { "The salt size should be $saltSize" }
|
||||||
require(keySize >= minKeySize) { "The key size should be at least $minKeySize bytes" }
|
require(keySize > minKeySize) { "The key size should be at least $keySize bytes" }
|
||||||
return when (complexity) {
|
return when (complexity) {
|
||||||
FixedLow -> Argon(
|
FixedLow -> Argon(
|
||||||
V2id_13,
|
V2id_13,
|
||||||
@ -257,96 +220,14 @@ sealed class KDF {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fast HKDF derivation for already strong byte material, implemented as described in
|
|
||||||
* [RFC 5869](https://www.rfc-editor.org/rfc/rfc5869).
|
|
||||||
*
|
|
||||||
* HKDF is suitable when [deriveFromBytes] receives source bytes that already contain enough entropy, for
|
|
||||||
* example a shared secret produced by a key agreement protocol, random master key material, or another
|
|
||||||
* high-entropy secret. It is fast by design: it does not slow down brute-force attempts and does not use
|
|
||||||
* extra memory. Do not use it for passwords, memorable phrases, short PINs, user-entered secrets, or other
|
|
||||||
* low-entropy inputs; use [Argon] for those instead.
|
|
||||||
*
|
|
||||||
* The derivation is deterministic for the tuple `(hash, salt, info, keySize, source)`. Store or recreate the
|
|
||||||
* same parameters to derive the same output again. [salt] and [info] are not secret, but changing either one
|
|
||||||
* changes the derived bytes.
|
|
||||||
*
|
|
||||||
* @property hash Hash function to use inside HMAC. The default is [Hash.Sha3_256]. [Hash.Blake2b],
|
|
||||||
* [Hash.Sha3_256], and [Hash.Sha3_384] are supported; [Hash.Sha3AndBlake] is intentionally rejected because
|
|
||||||
* it is a synthetic concatenated hash and has no well-defined HMAC block size.
|
|
||||||
* @property salt Optional extraction salt. Prefer a stable random salt when one is available and can be stored
|
|
||||||
* with the derived data. An empty salt is allowed by HKDF and is treated as a zero-filled salt of the selected
|
|
||||||
* hash output size.
|
|
||||||
* @property info Optional application-specific context string or binary label. Use it to domain-separate
|
|
||||||
* outputs derived from the same source, for example `"container encryption"`, `"header auth"`, or a protocol
|
|
||||||
* transcript hash. It may be empty when there is only one unambiguous use for the derived bytes.
|
|
||||||
* @property keySize Number of bytes to derive. It must be positive and no larger than `255 * hashLen`, where
|
|
||||||
* `hashLen` is the output size of [hash].
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
@SerialName("hkdf")
|
|
||||||
data class HKDF(
|
|
||||||
val hash: Hash = Hash.Sha3_256,
|
|
||||||
val salt: UByteArray = ubyteArrayOf(),
|
|
||||||
val info: UByteArray = ubyteArrayOf(),
|
|
||||||
@Unsigned
|
|
||||||
val keySize: Int,
|
|
||||||
) : KDF() {
|
|
||||||
|
|
||||||
init {
|
|
||||||
require(keySize > 0) { "HKDF key size should be positive" }
|
|
||||||
require(hash.supportsHmac) { "Hash $hash is not supported for HKDF" }
|
|
||||||
require(keySize <= hash.hkdfMaxOutputSize) {
|
|
||||||
"HKDF output for $hash is limited to ${hash.hkdfMaxOutputSize} bytes"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deriveKey(password: String): UByteArray =
|
|
||||||
throw UnsupportedOperationException("HKDF is not a password KDF; use Argon for passwords")
|
|
||||||
|
|
||||||
override fun deriveFromBytes(source: UByteArray): UByteArray =
|
|
||||||
hkdf(hash, source, salt, info, keySize)
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other !is HKDF) return false
|
|
||||||
|
|
||||||
if (hash != other.hash) return false
|
|
||||||
if (keySize != other.keySize) return false
|
|
||||||
if (!salt.contentEquals(other.salt)) return false
|
|
||||||
return info.contentEquals(other.info)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = hash.hashCode()
|
|
||||||
result = 31 * result + salt.contentHashCode()
|
|
||||||
result = 31 * result + info.contentHashCode()
|
|
||||||
result = 31 * result + keySize
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
fun creteDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF {
|
||||||
/**
|
return Argon.create(complexity, salt, keySize)
|
||||||
* Create default [Argon] KDF parameters. If [salt] is not exactly [Argon.saltSize], it is
|
|
||||||
* deterministically normalized with [Hash.Blake2b.deriveSaltFormBytes] before creating the KDF.
|
|
||||||
*/
|
|
||||||
fun createDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF {
|
|
||||||
val normalizedSalt = if (salt.size == Argon.saltSize) {
|
|
||||||
salt
|
|
||||||
} else {
|
|
||||||
Hash.Blake2b.deriveSaltFormBytes(salt, Argon.saltSize)
|
|
||||||
}
|
|
||||||
return Argon.create(complexity, normalizedSalt, keySize)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Use createDefault", ReplaceWith("createDefault(keySize, complexity, salt)"))
|
|
||||||
fun creteDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF =
|
|
||||||
createDefault(keySize, complexity, salt)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Instance(val kdf: KDF, val password: String)
|
data class Instance(val kdf: KDF, val password: String)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,8 +12,8 @@
|
|||||||
|
|
||||||
package net.sergeych.utools
|
package net.sergeych.utools
|
||||||
|
|
||||||
import kotlin.time.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlin.time.Instant
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
fun now(): Instant = Clock.System.now()
|
fun now(): Instant = Clock.System.now()
|
||||||
fun nowToSeconds(): Instant = Clock.System.now().truncateToSeconds()
|
fun nowToSeconds(): Instant = Clock.System.now().truncateToSeconds()
|
||||||
|
|||||||
@ -10,17 +10,25 @@
|
|||||||
|
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlin.time.Clock
|
import kotlinx.datetime.Clock
|
||||||
import net.sergeych.crypto2.Hash
|
import net.sergeych.crypto2.Hash
|
||||||
import net.sergeych.crypto2.initCrypto
|
import net.sergeych.crypto2.initCrypto
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import kotlin.random.nextUBytes
|
import kotlin.random.nextUBytes
|
||||||
import kotlin.test.Ignore
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertContentEquals
|
import kotlin.test.assertContentEquals
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
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 {
|
class HashTest {
|
||||||
@Test
|
@Test
|
||||||
fun testEqualMethods() {
|
fun testEqualMethods() {
|
||||||
@ -55,44 +63,17 @@ class HashTest {
|
|||||||
@Test
|
@Test
|
||||||
fun deriveSaltTest() = runTest {
|
fun deriveSaltTest() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
for (hash in Hash.entries) {
|
for( i in 2..257 ) {
|
||||||
for (i in 1..257) {
|
val x = Hash.Sha3AndBlake.deriveSalt("base one", i)
|
||||||
val x = hash.deriveSalt("base one", i)
|
val y = Hash.Sha3AndBlake.deriveSalt("base one", i)
|
||||||
val y = hash.deriveSalt("base one", i)
|
val z = Hash.Sha3AndBlake.deriveSalt("base two", i)
|
||||||
val z = hash.deriveSalt("base two", i)
|
assertContentEquals(x, y)
|
||||||
assertContentEquals(x, y)
|
assertFalse { x contentEquals z }
|
||||||
if (i > 1) assertFalse { x contentEquals z }
|
assertEquals(x.size, i)
|
||||||
assertEquals(i, x.size)
|
assertEquals(y.size, i)
|
||||||
assertEquals(i, y.size)
|
assertEquals(z.size, i)
|
||||||
assertEquals(i, z.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun deriveSaltFromBytesTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
val base = "base one".encodeToByteArray().asUByteArray()
|
|
||||||
for (hash in Hash.entries) {
|
|
||||||
for (i in listOf(1, 5, 31, 32, 33, 96, 97)) {
|
|
||||||
val fromString = hash.deriveSalt("base one", i)
|
|
||||||
val fromBytes = hash.deriveSaltFromBytes(base, i)
|
|
||||||
val fromTypoAlias = hash.deriveSaltFormBytes(base, i)
|
|
||||||
assertEquals(i, fromBytes.size)
|
|
||||||
assertContentEquals(fromString, fromBytes)
|
|
||||||
assertContentEquals(fromBytes, fromTypoAlias)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@ -8,16 +8,12 @@
|
|||||||
* real dot sergeych at gmail.
|
* real dot sergeych at gmail.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import net.sergeych.crypto2.Hash
|
|
||||||
import net.sergeych.crypto2.KDF
|
import net.sergeych.crypto2.KDF
|
||||||
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.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertIs
|
|
||||||
|
|
||||||
class KDFTest {
|
class KDFTest {
|
||||||
@Test
|
@Test
|
||||||
@ -59,159 +55,4 @@ class KDFTest {
|
|||||||
assertEquals(3, kk.size)
|
assertEquals(3, kk.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun complexityKdfForSizeInBytesTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
val size = KDF.Argon.minKeySize + 17
|
|
||||||
val expectedSalt = UByteArray(KDF.Argon.saltSize) { (it + 3).toUByte() }
|
|
||||||
|
|
||||||
val kdf = KDF.Complexity.FixedLow.kdfForSizeInBytes(size, expectedSalt)
|
|
||||||
assertIs<KDF.Argon>(kdf)
|
|
||||||
|
|
||||||
assertEquals(KDF.Argon.create(KDF.Complexity.FixedLow, expectedSalt, size), kdf)
|
|
||||||
assertEquals(size, kdf.keySize)
|
|
||||||
assertContentEquals(expectedSalt, kdf.salt)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun createDefaultNormalizesSaltTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
val size = KDF.Argon.minKeySize + 19
|
|
||||||
val shortSalt = ubyteArrayOf(1u, 2u, 3u, 4u, 5u)
|
|
||||||
val longSalt = UByteArray(KDF.Argon.saltSize + 7) { (it * 5).toUByte() }
|
|
||||||
val expectedShortSalt = Hash.Blake2b.deriveSaltFormBytes(shortSalt, KDF.Argon.saltSize)
|
|
||||||
val expectedLongSalt = Hash.Blake2b.deriveSaltFormBytes(longSalt, KDF.Argon.saltSize)
|
|
||||||
|
|
||||||
val shortKdf = KDF.createDefault(size, KDF.Complexity.FixedLow, shortSalt)
|
|
||||||
val longKdf = KDF.Complexity.FixedLow.kdfForSizeInBytes(size, longSalt)
|
|
||||||
|
|
||||||
assertIs<KDF.Argon>(shortKdf)
|
|
||||||
assertIs<KDF.Argon>(longKdf)
|
|
||||||
assertEquals(KDF.Argon.saltSize, shortKdf.salt.size)
|
|
||||||
assertEquals(KDF.Argon.saltSize, longKdf.salt.size)
|
|
||||||
assertContentEquals(expectedShortSalt, shortKdf.salt)
|
|
||||||
assertContentEquals(expectedLongSalt, longKdf.salt)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun deriveFromBytesTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
val salt = UByteArray(KDF.Argon.saltSize) { it.toUByte() }
|
|
||||||
val kdf = KDF.Argon.create(KDF.Complexity.Interactive, salt, 80)
|
|
||||||
val source = UByteArray(47) { (it * 3).toUByte() }
|
|
||||||
|
|
||||||
val b1 = kdf.deriveFromBytes(source)
|
|
||||||
val b2 = kdf.deriveFromBytes(source)
|
|
||||||
val b3 = kdf.deriveFromBytes(source + 1u)
|
|
||||||
|
|
||||||
assertEquals(80, b1.size)
|
|
||||||
assertContentEquals(b1, b2)
|
|
||||||
assertFalse { b1 contentEquals b3 }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun complexityDeriveFromBytesTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
val salt = UByteArray(KDF.Argon.saltSize) { (it + 1).toUByte() }
|
|
||||||
val source = UByteArray(23) { (it + 7).toUByte() }
|
|
||||||
|
|
||||||
val b1 = KDF.Complexity.Interactive.derive(source, salt, 64)
|
|
||||||
val b2 = KDF.Argon.create(KDF.Complexity.Interactive, salt, 64).deriveFromBytes(source)
|
|
||||||
|
|
||||||
assertContentEquals(b1, b2)
|
|
||||||
assertEquals(64, b1.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun deriveFromBytesUsesKeySizeTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
val salt = UByteArray(KDF.Argon.saltSize) { it.toUByte() }
|
|
||||||
val source = ubyteArrayOf(1u, 2u, 3u)
|
|
||||||
val shortKdf = KDF.Argon.create(KDF.Complexity.Interactive, salt, 64)
|
|
||||||
val longKdf = KDF.Argon.create(KDF.Complexity.Interactive, salt, 96)
|
|
||||||
|
|
||||||
assertEquals(64, shortKdf.deriveFromBytes(source).size)
|
|
||||||
assertEquals(96, longKdf.deriveFromBytes(source).size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun hkdfDeriveFromBytesTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
val source = "strong source bytes with enough entropy".encodeToUByteArray()
|
|
||||||
val salt = "stored public salt".encodeToUByteArray()
|
|
||||||
val info = "crypto2:test".encodeToUByteArray()
|
|
||||||
val kdf = KDF.HKDF(Hash.Sha3_256, salt, info, 80)
|
|
||||||
|
|
||||||
val b1 = kdf.deriveFromBytes(source)
|
|
||||||
val b2 = kdf.deriveFromBytes(source)
|
|
||||||
val b3 = kdf.deriveFromBytes(source + 1u)
|
|
||||||
val b4 = kdf.copy(info = "crypto2:other".encodeToUByteArray()).deriveFromBytes(source)
|
|
||||||
val expected = hexToUBytes(
|
|
||||||
"580cfd4eb588312097c9e43852d62bec9aff988591e2e5f721a2facc80c3c494" +
|
|
||||||
"d3487b81e19f71611bdebb9983b94e058c9f0c38bc226b9b895eb554d17446b" +
|
|
||||||
"672e12c7e2c36683807cd1e8d67ada0f7"
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals(80, b1.size)
|
|
||||||
assertContentEquals(expected, b1)
|
|
||||||
assertContentEquals(b1, b2)
|
|
||||||
assertFalse { b1 contentEquals b3 }
|
|
||||||
assertFalse { b1 contentEquals b4 }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun hkdfParametersCompareArraysByContentTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
val k1 = KDF.HKDF(
|
|
||||||
Hash.Sha3_384,
|
|
||||||
"salt".encodeToUByteArray(),
|
|
||||||
"info".encodeToUByteArray(),
|
|
||||||
42
|
|
||||||
)
|
|
||||||
val k2 = KDF.HKDF(
|
|
||||||
Hash.Sha3_384,
|
|
||||||
"salt".encodeToUByteArray(),
|
|
||||||
"info".encodeToUByteArray(),
|
|
||||||
42
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals(k1, k2)
|
|
||||||
assertEquals(k1.hashCode(), k2.hashCode())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun hkdfRejectsInvalidParametersTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
|
|
||||||
assertThrows<IllegalArgumentException> {
|
|
||||||
KDF.HKDF(Hash.Sha3_256, keySize = 0)
|
|
||||||
}
|
|
||||||
assertThrows<IllegalArgumentException> {
|
|
||||||
KDF.HKDF(Hash.Sha3AndBlake, keySize = 32)
|
|
||||||
}
|
|
||||||
assertThrows<IllegalArgumentException> {
|
|
||||||
KDF.HKDF(Hash.Sha3_256, keySize = 32 * 255 + 1)
|
|
||||||
}
|
|
||||||
assertThrows<IllegalArgumentException> {
|
|
||||||
KDF.HKDF(Hash.Sha3_256, keySize = 32).deriveFromBytes(ubyteArrayOf())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun hkdfDoesNotDeriveFromPasswordTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
val kdf = KDF.HKDF(keySize = 32)
|
|
||||||
|
|
||||||
assertThrows<UnsupportedOperationException> {
|
|
||||||
kdf.deriveKey("password")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hexToUBytes(hex: String): UByteArray {
|
|
||||||
require(hex.length % 2 == 0) { "hex string should have even length" }
|
|
||||||
return UByteArray(hex.length / 2) {
|
|
||||||
hex.substring(it * 2, it * 2 + 2).toInt(16).toUByte()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -11,6 +11,7 @@
|
|||||||
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
|
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
|
||||||
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
|
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import net.sergeych.bipack.BipackDecoder
|
import net.sergeych.bipack.BipackDecoder
|
||||||
import net.sergeych.bipack.BipackEncoder
|
import net.sergeych.bipack.BipackEncoder
|
||||||
@ -82,48 +83,6 @@ class KeysTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun seededPrivateKeysTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
val source = "stable high entropy key material placeholder".encodeToUByteArray() + randomUBytes(128)
|
|
||||||
val data = "seeded generation works".encodeToUByteArray()
|
|
||||||
|
|
||||||
val signingSeed = SigningSecretKey.seedFromBytes(source)
|
|
||||||
val decryptingSeed = DecryptingSecretKey.seedFromBytes(source)
|
|
||||||
assertEquals(SigningSecretKey.seedLength, signingSeed.size)
|
|
||||||
assertEquals(DecryptingSecretKey.seedLength, decryptingSeed.size)
|
|
||||||
assertFalse(signingSeed contentEquals decryptingSeed)
|
|
||||||
|
|
||||||
val signingKey1 = SigningSecretKey.fromSeed(signingSeed)
|
|
||||||
val signingKey2 = SigningSecretKey.fromSeed(signingSeed)
|
|
||||||
assertEquals(signingKey1, signingKey2)
|
|
||||||
val signature = signingKey1.sign(data)
|
|
||||||
assertTrue(signingKey2.verifyingKey.verify(signature, data))
|
|
||||||
|
|
||||||
val decryptingKey1 = DecryptingSecretKey.fromSeed(decryptingSeed)
|
|
||||||
val decryptingKey2 = DecryptingSecretKey.fromSeed(decryptingSeed)
|
|
||||||
assertEquals(decryptingKey1.publicKey, decryptingKey2.publicKey)
|
|
||||||
assertContentEquals(data, decryptingKey2.decrypt(decryptingKey1.publicKey.encrypt(data)))
|
|
||||||
|
|
||||||
val universalSeed = UniversalPrivateKey.seedFromBytes(source)
|
|
||||||
assertEquals(UniversalPrivateKey.seedLength, universalSeed.size)
|
|
||||||
val universalKey1 = UniversalPrivateKey.fromSeed(universalSeed)
|
|
||||||
val universalKey2 = UniversalPrivateKey.fromSeed(universalSeed)
|
|
||||||
assertEquals(universalKey1.publicKey, universalKey2.publicKey)
|
|
||||||
assertTrue(universalKey2.publicKey.verify(universalKey1.sign(data), data))
|
|
||||||
assertContentEquals(data, universalKey2.decrypt(universalKey1.publicKey.encrypt(data)))
|
|
||||||
|
|
||||||
assertFailsWith<IllegalArgumentException> {
|
|
||||||
SigningSecretKey.fromSeed(signingSeed.dropLast(1).toUByteArray())
|
|
||||||
}
|
|
||||||
assertFailsWith<IllegalArgumentException> {
|
|
||||||
DecryptingSecretKey.fromSeed(decryptingSeed.dropLast(1).toUByteArray())
|
|
||||||
}
|
|
||||||
assertFailsWith<IllegalArgumentException> {
|
|
||||||
UniversalPrivateKey.fromSeed(universalSeed.dropLast(1).toUByteArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun secretEncryptTest() = runTest {
|
fun secretEncryptTest() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
@ -159,44 +118,6 @@ class KeysTest {
|
|||||||
assertContentEquals(src, k1.decrypt(k1.encrypt(src, 7..117)))
|
assertContentEquals(src, k1.decrypt(k1.encrypt(src, 7..117)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun symmetricKeyCompactTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
val key = SymmetricKey.new()
|
|
||||||
val otherKey = SymmetricKey.new()
|
|
||||||
val src = "readme.md".encodeToUByteArray()
|
|
||||||
val nonce = key.randomNonce()
|
|
||||||
val cipher = key.encryptCompactWithNonce(src, nonce)
|
|
||||||
|
|
||||||
assertEquals(src.size + 16, cipher.size)
|
|
||||||
assertTrue(cipher.size < key.encryptWithNonce(src, nonce).size)
|
|
||||||
assertTrue(cipher.size < key.encrypt(src).size)
|
|
||||||
assertContentEquals(src, key.decryptCompactWithNonce(cipher, nonce))
|
|
||||||
|
|
||||||
assertThrows<DecryptionFailedException> {
|
|
||||||
otherKey.decryptCompactWithNonce(cipher, nonce)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThrows<DecryptionFailedException> {
|
|
||||||
val n2 = nonce.copyOf()
|
|
||||||
n2[4] = n2[4].inv()
|
|
||||||
key.decryptCompactWithNonce(cipher, n2)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThrows<DecryptionFailedException> {
|
|
||||||
val c2 = cipher.copyOf()
|
|
||||||
c2[3] = c2[3].inv()
|
|
||||||
key.decryptCompactWithNonce(c2, nonce)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThrows<IllegalArgumentException> {
|
|
||||||
key.encryptCompactWithNonce(src, nonce.dropLast(1).toUByteArray())
|
|
||||||
}
|
|
||||||
assertThrows<DecryptionFailedException> {
|
|
||||||
key.decryptCompactWithNonce(cipher, nonce.dropLast(1).toUByteArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun keyExchangeTest() = runTest {
|
fun keyExchangeTest() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
@ -221,26 +142,6 @@ class KeysTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun keyExchangeSerializationTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
val ske = SafeKeyExchange()
|
|
||||||
val packedExchange = pack(ske)
|
|
||||||
|
|
||||||
val cke = SafeKeyExchange()
|
|
||||||
val clientSessionKey = cke.clientSessionKey(ske.publicKey)
|
|
||||||
|
|
||||||
val restoredExchange = unpack<SafeKeyExchange>(packedExchange)
|
|
||||||
assertEquals(ske.publicKey, restoredExchange.publicKey)
|
|
||||||
|
|
||||||
val serverSessionKey = restoredExchange.serverSessionKey(cke.publicKey)
|
|
||||||
|
|
||||||
val src = "Hello after restore!"
|
|
||||||
assertEquals(src, serverSessionKey.decryptString(clientSessionKey.encrypt(src)))
|
|
||||||
assertEquals(src, clientSessionKey.decryptString(serverSessionKey.encrypt(src)))
|
|
||||||
assertContentEquals(clientSessionKey.sessionTag, serverSessionKey.sessionTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun asymmetricKeyTest() = runTest {
|
fun asymmetricKeyTest() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlin.time.Instant
|
import kotlinx.datetime.Instant
|
||||||
import net.sergeych.crypto2.initCrypto
|
import net.sergeych.crypto2.initCrypto
|
||||||
import net.sergeych.utools.nowToSeconds
|
import net.sergeych.utools.nowToSeconds
|
||||||
import net.sergeych.utools.pack
|
import net.sergeych.utools.pack
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user