Compare commits

...
39 Commits
Author SHA1 Message Date
sergeych 66254f8fa6 fixed SafeKeyEXchange.SessionKey serialization issue 2026-06-20 23:55:01 +03:00
sergeych 84a3e82216 added HKDF, version will be 0.9.3 2026-06-07 13:12:03 +03:00
sergeych d402afe1bd version bump to 0.9.2 2026-05-31 00:09:29 +03:00
sergeych ff494ab4ef Fix salt derivation and KDF byte derivation 2026-05-30 15:48:48 +03:00
sergeych 2d55af4e3b fixed kdfForSizeInBytes unneeded parameter 2026-05-30 12:09:07 +03:00
sergeych a77fdd79c1 Added kdfForSizeInBytes method to KDF with tests and a new factory method to UniversalRing. 2026-05-30 12:07:22 +03:00
sergeych d6b171229f Added support for SafeKeyExchange serialization/deserialization and associated tests. 2026-05-29 15:43:44 +03:00
sergeych cd57627cdb fixed key exchange docs error 2026-05-29 15:11:43 +03:00
sergeych 5c3ff72123 +KDF derive from bytes 2026-05-29 13:22:52 +03:00
sergeych d0f1fffe6d Add seeded private key generation 2026-05-29 12:51:14 +03:00
sergeych 8015a4310b Added support for compact encryption/decryption with caller-provided nonce in SymmetricKey and accompanying tests. Bumped version to 0.9.1-SNAPSHOT. 2026-05-28 18:40:17 +03:00
sergeych 13dff8d760 v0.9.0: kotlin 2.2.21, kotlin.time fix for deprecated methods in kotlinx.datetime. Should work ok on mac/ios 2025-12-16 19:13:27 +01:00
sergeych 3bd06ac7ff added Container.decryptWithPassword 2025-12-04 12:14:07 +01:00
sergeych ca9ab0f7a0 0.8.5: upgrade kotlin & dependencies 2025-10-24 18:05:25 +02:00
sergeych b86ac6f00b 0.8.4 for all platforms 2025-03-27 00:17:59 +03:00
sergeych 9d338d2f13 merging 2025-03-27 00:14:32 +03:00
sergeych 5228be33ee fixed README.md 2025-03-26 23:45:31 +03:00
sergeych 528439f61d 0.8.3 for all platforms 2025-03-26 23:38:40 +03:00
sergeych 7c97a843e7 Merge pull request 'multiplatform-crypto-libsodium-bindings version with all targets' (#11) from YoungBlood/crypto2:master into master
Reviewed-on: #11
2025-03-25 21:05:37 +03:00
kildishevps 1a81cd5110 multiplatform-crypto-libsodium-bindings version with all targets 2025-03-24 00:53:28 +03:00
sergeych 7d3e396cf7 dependency simplified 2025-03-16 22:29:22 +03:00
sergeych 85b13ed8ca EncryptedKVStorage: can destroy exisint creating one with a new key 2025-03-16 16:53:05 +03:00
sergeych fbbe4d3a34 EncryptedKVStorage: can destroy exisint creating one with a new key 2025-03-16 16:52:26 +03:00
sergeych 875c0f7a50 fix #10 KDF.Complexity derivation functions now require salt 2025-03-12 23:39:26 +03:00
sergeych fe6190eb8d +EncryptedKVStorage 2025-03-12 23:00:40 +03:00
sergeych 776f4e75ff 0.8.3-SNAPSHOT use updated bintools 2025-03-08 02:11:13 +03:00
sergeych fa7263b0e7 0.8.3-SNAPSHOT uaw mp_bintools bytechunk for compatibility 2025-03-08 01:53:35 +03:00
sergeych bd81f88dd8 0.8.2-SNAPSHOT support for wasmJs using youngblood, no mingw yet 2025-03-07 15:14:54 +03:00
sergeych 6fcf7841a7 Merge pull request 'Change multiplatform-crypto-libsodium-bindings version and bring wasmJs back' (#9) from YoungBlood/crypto2:master into master
Reviewed-on: #9
2025-03-07 09:31:04 +03:00
kildishevps 4748ea0d65 Change commonMain multiplatform-crypto-libsodium-bindings version and bring wasmJs back 2025-03-03 12:48:10 +03:00
sergeych e2d4fb07ad 0.7.4-SNAPSHOT back to no wasm; public/secret key renamed with compatibility aliases and deprecation warnings 2025-02-24 09:26:11 +03:00
sergeych d180da309b added copyright, license mention and other #10x2linus stuff. 2025-02-20 12:21:42 +03:00
sergeych c9e3c57ee2 0.8.1 started: experimental support of the wasmJS target. 2025-02-13 10:04:16 +03:00
sergeych 81e02ac88e Merge pull request 'Fixing multiplatform-crypto-libsodium-bindings dependencies' (#8) from YoungBlood/crypto2:master into master
Reviewed-on: #8
2025-02-13 09:54:58 +03:00
kildishevps e8d6b2fc02 Fixing multiplatform-crypto-libsodium-bindings dependencies 2025-02-09 21:59:28 +03:00
sergeych 277dc62553 Merge pull request 'Add wasmJs target' (#7) from YoungBlood/crypto2:master into master
Reviewed-on: #7
2025-02-09 16:57:38 +03:00
PlaceboAddict 7e52a72c6a Add wasmJs target 2025-02-05 16:39:10 +03:00
sergeych 7fa3ab1ca8 seal now allows up to 45s in future to tolerate some time difference 2025-02-03 17:13:28 +03:00
sergeych 242cc7d0f5 more tests 2024-11-29 15:42:54 +07:00
80 changed files with 1926 additions and 343 deletions

No files matched your search

+1
View File
@@ -9,6 +9,7 @@ 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
View File
@@ -1,10 +1,3 @@
# 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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
-8
View File
@@ -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>
+24
View File
@@ -1,5 +1,29 @@
<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
View File
@@ -5,6 +5,7 @@
<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$" />
@@ -12,5 +13,6 @@
</option> </option>
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
<option name="parallelModelFetch" value="true" />
</component> </component>
</project> </project>
-7
View File
@@ -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>
-6
View File
@@ -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>
+2 -1
View File
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<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_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" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>
+28 -1
View File
@@ -2,6 +2,16 @@
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.
@@ -21,7 +31,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.7.1-SNAPSHOT") import("net.sergeych:crypto2:0.8.4")
} }
``` ```
@@ -36,6 +46,10 @@ Please see the current documentation [here](https://code.sergeych.net/docs/crypt
- All moder browsers, including mobile - All moder browsers, including mobile
- Node.js - Node.js
## WasmJs
- All moder browsers, including mobile
## JVM ## JVM
- Android - Android
@@ -96,6 +110,10 @@ 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.
@@ -110,4 +128,13 @@ Secret key encryption and signing/verifying uses Edwards curves 25519 algorithms
- SHA3 256, 384, more are on the way. - SHA3 256, 384, more are on the way.
- CRC-protected binary ID with magic numbers to implement human-friendly IDS with type checks - CRC-protected binary ID with magic numbers to implement human-friendly IDS with type checks
## Licensing
# Licensing
This is work in progress, not yet moved to public domain;
you need to obtain a license from https://8-rays.dev or [Sergey Chernov]. For open source projects it will most be free on some special terms.
It will be moved to open source; we also guarantee that it will be moved to open source immediately if the software export restrictions will be lifted. We do not support such practices here at 8-rays.dev and assume open source must be open.
[Sergey Chernov]: https://t.me/real_sergeych
+10
View File
@@ -1,4 +1,14 @@
#!/bin/bash #!/bin/bash
#
# 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.
#
set -e set -e
./gradlew dokkaHtml ./gradlew dokkaHtml
rsync -avz ./build/dokka/* code.sergeych.net:/bigstore/sergeych_pub/code/docs/crypto2 rsync -avz ./build/dokka/* code.sergeych.net:/bigstore/sergeych_pub/code/docs/crypto2
+38 -21
View File
@@ -1,30 +1,38 @@
/*
* 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.
*/
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
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.0.20" kotlin("multiplatform") version "2.4.0"
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20" id("org.jetbrains.kotlin.plugin.serialization") version "2.4.0"
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.7.1-SNAPSHOT" version = "0.9.4-SNAPSHOT"
repositories { repositories {
mavenCentral() mavenCentral()
maven("https://maven.universablockchain.com/") maven("https://maven.universablockchain.com/")
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven") maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
maven("https://gitea.sergeych.net/api/packages/YoungBlood/maven")
mavenLocal() mavenLocal()
} }
kotlin { kotlin {
jvm { jvmToolchain(21)
@OptIn(ExperimentalKotlinGradlePluginApi::class) jvm()
compilerOptions {
jvmTarget = JvmTarget.JVM_11
}
}
js { js {
browser() browser()
nodejs() nodejs()
@@ -38,36 +46,37 @@ kotlin {
iosArm64() iosArm64()
iosSimulatorArm64() iosSimulatorArm64()
mingwX64() mingwX64()
// @OptIn(ExperimentalWasmDsl::class) @OptIn(ExperimentalWasmDsl::class)
// wasmJs() //no libsodium bindings yet (strangely) wasmJs {
// val ktor_version = "2.3.6" browser()
}
sourceSets { sourceSets {
all { all {
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.8.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") 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(project.dependencies.platform("org.kotlincrypto.hash:bom:0.5.1"))
implementation("org.kotlincrypto.hash:sha3") implementation("org.kotlincrypto.hash:sha3")
api("com.ionspin.kotlin:bignum:0.3.9") api("com.ionspin.kotlin:bignum:0.3.10")
api("net.sergeych:mp_bintools:0.1.7") api("net.sergeych:mp_bintools:0.3.2")
api("net.sergeych:mp_stools:1.5.1")
} }
} }
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.8.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
} }
} }
val native by creating { val native by creating {
@@ -80,7 +89,7 @@ kotlin {
} }
} }
val jvmTest by getting val jvmTest by getting
for (platform in listOf(linuxMain, macosMain, iosMain, mingwMain)) for (platform in listOf(linuxX64Main, linuxArm64Main, macosX64Main, macosArm64Main, iosX64Main, iosArm64Main, iosSimulatorArm64Main, mingwX64Main))
platform { dependsOn(native) } platform { dependsOn(native) }
} }
} }
@@ -103,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 { tasks.dokkaHtml.configure {
outputDirectory.set(buildDir.resolve("dokka")) outputDirectory.set(buildDir.resolve("dokka"))
dokkaSourceSets { dokkaSourceSets {
+14
View File
@@ -1 +1,15 @@
#
# 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.
#
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
+11 -1
View File
@@ -1,5 +1,15 @@
#
# 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.
#
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+5 -11
View File
@@ -1,19 +1,13 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # You may use, distribute and modify this code under the
# you may not use this file except in compliance with the License. # terms of the private license, which you must obtain from the author
# You may obtain a copy of the License at
# #
# https://www.apache.org/licenses/LICENSE-2.0 # To obtain the license, contact the author: https://t.me/real_sergeych or email to
# # real dot sergeych at gmail.
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# #
############################################################################## ##############################################################################
+10
View File
@@ -1,3 +1,13 @@
/*
* 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.
*/
pluginManagement { pluginManagement {
repositories { repositories {
mavenCentral() mavenCentral()
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.box.Box import com.ionspin.kotlin.crypto.box.Box
@@ -14,10 +24,10 @@ import net.sergeych.crypto2.Asymmetric.generateKeys
* *
* ## How to * ## How to
* *
* - [SecretKey.new] to create a secret key that includes [SecretKey.publicKey]. * - [DecryptingSecretKey.new] to create a secret key that includes [DecryptingSecretKey.publicKey].
* [generateKeys] also makes the pair. * [generateKeys] also makes the pair.
* - [PublicKey] provides encryption, anonymous or authenticated. * - [EncryptingPublicKey] provides encryption, anonymous or authenticated.
* - [SecretKey] provides authenticated decryption of what [PublicKey] was encrypted with. * - [DecryptingSecretKey] provides authenticated decryption of what [EncryptingPublicKey] was encrypted with.
* - [Message] is a serializable container with all necessary data to decrypt public-key encrypted data it. * - [Message] is a serializable container with all necessary data to decrypt public-key encrypted data it.
* *
* __Algorithms:__ * __Algorithms:__
@@ -31,8 +41,8 @@ object Asymmetric {
/** /**
* Encrypted message holder. * Encrypted message holder.
* *
* Do not instantiate it directly, use [PublicKey.encryptMessage], [PublicKey.encryptAnonymousMessage], etc. * Do not instantiate it directly, use [EncryptingPublicKey.encryptMessage], [EncryptingPublicKey.encryptAnonymousMessage], etc.
* instead. Also [SecretKey.decrypt] can be used to decrypt it same as [decrypt] or [decryptWithSenderKey]. * instead. Also [DecryptingSecretKey.decrypt] can be used to decrypt it same as [decrypt] or [decryptWithSenderKey].
* *
* To successfully decrypt the message, it is necessary to know a sender public key, and non-secret nonce. * To successfully decrypt the message, it is necessary to know a sender public key, and non-secret nonce.
* This class carries all this information; serialize and pass it to the recipient. * This class carries all this information; serialize and pass it to the recipient.
@@ -41,12 +51,12 @@ object Asymmetric {
class Message( class Message(
private val nonce: UByteArray, private val nonce: UByteArray,
private val encryptedMessage: UByteArray, private val encryptedMessage: UByteArray,
val senderPublicKey: PublicKey, val senderPublicKey: EncryptingPublicKey,
) { ) {
/** /**
* Decrypt the message, same as [SecretKey.decrypt] * Decrypt the message, same as [DecryptingSecretKey.decrypt]
*/ */
fun decrypt(recipientKey: SecretKey): UByteArray { fun decrypt(recipientKey: DecryptingSecretKey): UByteArray {
return decryptWithSenderKey(senderPublicKey, recipientKey) return decryptWithSenderKey(senderPublicKey, recipientKey)
} }
@@ -54,7 +64,7 @@ object Asymmetric {
* Decrypt a message which is not include sender's public key (which should somehow be * Decrypt a message which is not include sender's public key (which should somehow be
* known to the recipient). Use it if [senderPublicKey] is null. * known to the recipient). Use it if [senderPublicKey] is null.
*/ */
fun decryptWithSenderKey(senderKey: PublicKey, recipientKey: SecretKey): UByteArray { fun decryptWithSenderKey(senderKey: EncryptingPublicKey, recipientKey: DecryptingSecretKey): UByteArray {
return try { return try {
WithFill.decode( WithFill.decode(
Box.openEasy(encryptedMessage, nonce, senderKey.keyBytes, recipientKey.keyBytes) Box.openEasy(encryptedMessage, nonce, senderKey.keyBytes, recipientKey.keyBytes)
@@ -79,19 +89,19 @@ object Asymmetric {
/** /**
* Encrypt the [plainData] using [from] sender for [recipient] public key. Note that to decrypt it * Encrypt the [plainData] using [from] sender for [recipient] public key. Note that to decrypt it
* the [SecretKey] that corresponds to the [recipient] public key is needed, Sender can't decrypt the message! * the [DecryptingSecretKey] that corresponds to the [recipient] public key is needed, Sender can't decrypt the message!
* *
* The authenticated encryption is used, is the message _is successfully decrypted_, it also means that * The authenticated encryption is used, is the message _is successfully decrypted_, it also means that
* it was signed by the sender, whose public key is known at the decryption time. * it was signed by the sender, whose public key is known at the decryption time.
* *
* When it is important not to provide senders' key, use [PublicKey.encryptAnonymousMessage]. * When it is important not to provide senders' key, use [EncryptingPublicKey.encryptAnonymousMessage].
* *
* @param from the senders' secret key. * @param from the senders' secret key.
* @param recipient the recipients' public key. * @param recipient the recipients' public key.
* @param plainData data to encrypt * @param plainData data to encrypt
*/ */
internal fun createMessage( internal fun createMessage(
from: SecretKey, recipient: PublicKey, plainData: UByteArray, from: DecryptingSecretKey, recipient: EncryptingPublicKey, plainData: UByteArray,
nonce: UByteArray = randomNonce(), nonce: UByteArray = randomNonce(),
): Message { ): Message {
return Message( return Message(
@@ -104,8 +114,8 @@ object Asymmetric {
private fun randomNonce(): UByteArray = randomUBytes(crypto_box_NONCEBYTES) private fun randomNonce(): UByteArray = randomUBytes(crypto_box_NONCEBYTES)
fun generateKeys() = SecretKey.generateKeys() fun generateKeys() = DecryptingSecretKey.generateKeys()
fun newSecretKey() = SecretKey.new() fun newSecretKey() = DecryptingSecretKey.new()
val nonceBytesLength = crypto_box_NONCEBYTES val nonceBytesLength = crypto_box_NONCEBYTES
@@ -115,4 +125,4 @@ object Asymmetric {
* Shortcut type: a pair of sender secret key and recipient private key could be used so * Shortcut type: a pair of sender secret key and recipient private key could be used so
* simplify such interfaces * simplify such interfaces
*/ */
typealias AsymmetricEncryptionPair = Pair<SecretKey?, PublicKey> typealias AsymmetricEncryptionPair = Pair<DecryptingSecretKey?, EncryptingPublicKey>
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.util.encodeToUByteArray import com.ionspin.kotlin.crypto.util.encodeToUByteArray
@@ -48,7 +58,7 @@ open class BinaryId(
/** /**
* Bad format (crc does not match) * Bad format (crc does not match)
*/ */
class InvalidException(text: String,reason: Throwable?=null) : IllegalArgumentException(text,reason) class InvalidException(text: String, reason: Throwable? = null) : IllegalArgumentException(text, reason)
/** /**
* Attempt to compare binary ids with different magic. In this case only [equals] * Attempt to compare binary ids with different magic. In this case only [equals]
@@ -70,32 +80,32 @@ open class BinaryId(
rest.last().toInt() rest.last().toInt()
} }
private val innerData: UByteArray by lazy { id.sliceArray( 1..< id.size-1 ) } private val innerData: UByteArray by lazy { id.sliceArray(1..<id.size - 1) }
/** /**
* The ID body: all the bytes except check and magic. ID bytes could carry useful information. * The ID body: all the bytes except check and magic. ID bytes could carry useful information.
* *
* - `id.size` is [body] size + 2 (see [BinaryId] inner structure) * - `id.size` is [body] size + 2 (see [BinaryId] inner structure)
*/ */
val body: UByteArray by lazy { id.sliceArray( 0 until id.size-2 ) } val body: UByteArray by lazy { id.sliceArray(0 until id.size - 2) }
val asVerifyingKey: VerifyingKey by lazy { val asVerifyingKey: VerifyingKey by lazy {
if( magic != KeysmagicNumber.defaultVerifying.ordinal) if (magic != KeysmagicNumber.defaultVerifying.ordinal)
throw InvalidException("It is not a veryfing key: magic=$magic, required ${KeysmagicNumber.defaultVerifying.ordinal}") throw InvalidException("It is not a veryfing key: magic=$magic, required ${KeysmagicNumber.defaultVerifying.ordinal}")
check(body.size == 32) check(body.size == 32)
VerifyingPublicKey(body) VerifyingPublicKey(body)
} }
/** /**
* Try to recnstruct a [PublicKey] from [id] bytes. For such keys, [PublicKey.id] and [SecretKey.id] * Try to recnstruct a [EncryptingPublicKey] from [id] bytes. For such keys, [EncryptingPublicKey.id] and [DecryptingSecretKey.id]
* are made from public key bytes so it could be restored from such an ID * are made from public key bytes so it could be restored from such an ID
* *
*/ */
val asPublicKey: PublicKey by lazy { val asPublicKey: EncryptingPublicKey by lazy {
if( magic != KeysmagicNumber.defaultAssymmetric.ordinal) if (magic != KeysmagicNumber.defaultAssymmetric.ordinal)
throw InvalidException("It is not a veryfing key: magic=$magic, required ${KeysmagicNumber.defaultAssymmetric.ordinal}") throw InvalidException("It is not a veryfing key: magic=$magic, required ${KeysmagicNumber.defaultAssymmetric.ordinal}")
check(body.size == 32) check(body.size == 32)
PublicKey(body) EncryptingPublicKey(body)
} }
override fun toString(): String = id.encodeToBase64Url() override fun toString(): String = id.encodeToBase64Url()
@@ -157,8 +167,8 @@ open class BinaryId(
} }
@Suppress("unused") @Suppress("unused")
fun createRandom(magicNumber: Int, size: Int=16) = fun createRandom(magicNumber: Int, size: Int = 16) =
createFromBytes(magicNumber, Random.Default.nextBytes(size-2)) createFromBytes(magicNumber, Random.Default.nextBytes(size - 2))
/** /**
* Encode a string as UTF and create a binaryId from its bytes and provided magic. * Encode a string as UTF and create a binaryId from its bytes and provided magic.
@@ -1,90 +0,0 @@
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)
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
@@ -24,8 +34,8 @@ import net.sergeych.crypto2.Container.Companion.createWith
* *
* Some rules: * Some rules:
* *
* When adding public key recipient, it is faster to use your known [SecretKey], but you * When adding public key recipient, it is faster to use your known [DecryptingSecretKey], but you
* can stay anonymous by just adding [PublicKey] only. * can stay anonymous by just adding [EncryptingPublicKey] only.
* *
* Put your data in [SealedBox] if you need to authenticate message origin and timestamp, then put * Put your data in [SealedBox] if you need to authenticate message origin and timestamp, then put
* the sealed box in the [Container], this will conceal signers from attack. In the case you need to * the sealed box in the [Container], this will conceal signers from attack. In the case you need to
@@ -107,7 +117,7 @@ sealed class Container {
* Add e key to the __decrypted__ container. The new container is also decrypted so you can add * Add e key to the __decrypted__ container. The new container is also decrypted so you can add
* more keys, etc. * more keys, etc.
*/ */
operator fun plus(recipient: PublicKey) = addRecipients { key(recipient) } operator fun plus(recipient: EncryptingPublicKey) = addRecipients { key(recipient) }
/** /**
* Add e key to the __decrypted__ container. The new container is also decrypted so you can add * Add e key to the __decrypted__ container. The new container is also decrypted so you can add
@@ -119,7 +129,7 @@ sealed class Container {
* Add e key to the __decrypted__ container. The new container is also decrypted so you can add * Add e key to the __decrypted__ container. The new container is also decrypted so you can add
* more keys, etc. * more keys, etc.
*/ */
operator fun plus(pair: Pair<SecretKey, PublicKey>) = addRecipients { key(pair) } operator fun plus(pair: Pair<DecryptingSecretKey, EncryptingPublicKey>) = addRecipients { key(pair) }
/** /**
* Update the data in the decrypted container. It keeps the same set of keys and update * Update the data in the decrypted container. It keeps the same set of keys and update
@@ -145,15 +155,9 @@ sealed class Container {
abstract val decryptedWithKeyId: KeyId? abstract val decryptedWithKeyId: KeyId?
/** /**
* If the container _is decrypted by the [PublicKey]_, e.g., using secret key encryption, * When the container is decrypted, the key that unlocked it, otherwise null.
* contains the [PublicKey] that corresponds the [SecretKey] used while encrypting, this
* authenticating the sender party cryptographically. This key could be used to encrypt
* the response to be visible to the sender only; the sender, providing it kept his secret key,
* could decrypt it.
*/ */
@Transient abstract val decryptedWithKey: UniversalKey?
var authorisedByKey: PublicKey? = null
protected set
/** /**
* List of [KeyId] of the keys that unlocked the container, in the same order used for encryption.. * List of [KeyId] of the keys that unlocked the container, in the same order used for encryption..
@@ -175,7 +179,8 @@ sealed class Container {
) : Container() { ) : Container() {
@Transient @Transient
private var decryptedWithKey: DecryptingKey? = null override var decryptedWithKey: UniversalKey? = null
private set
override val decryptedWithKeyId: KeyId? override val decryptedWithKeyId: KeyId?
get() = decryptedWithKey?.id get() = decryptedWithKey?.id
@@ -192,10 +197,7 @@ sealed class Container {
if (k.id == keyId) { if (k.id == keyId) {
kotlin.runCatching { k.decrypt(encryptedMessage) }.getOrNull()?.let { kotlin.runCatching { k.decrypt(encryptedMessage) }.getOrNull()?.let {
decryptedData = it decryptedData = it
decryptedWithKey = k decryptedWithKey = k as UniversalKey
if( k is SecretKey) {
authorisedByKey = Asymmetric.Message.decode(encryptedMessage).senderPublicKey
}
return it return it
} }
} }
@@ -216,7 +218,7 @@ sealed class Container {
// otherwise, we don't know the encryption key and will try to derive it // otherwise, we don't know the encryption key and will try to derive it
// from the decryption key: // from the decryption key:
when (val k = decryptedWithKey!!) { when (val k = decryptedWithKey!!) {
is SecretKey -> { is DecryptingSecretKey -> {
key(k.publicKey) key(k.publicKey)
} }
@@ -259,12 +261,12 @@ sealed class Container {
constructor(key: EncryptingKey, encodeMainKey: UByteArray) : constructor(key: EncryptingKey, encodeMainKey: UByteArray) :
this(key.id, key.encrypt(encodeMainKey)) this(key.id, key.encrypt(encodeMainKey))
constructor(sender: SecretKey?, recipient: PublicKey, encodeMainKey: UByteArray) : constructor(sender: DecryptingSecretKey?, recipient: EncryptingPublicKey, encodeMainKey: UByteArray) :
this( this(
recipient.id, recipient.id,
recipient.encryptMessage( recipient.encryptMessage(
encodeMainKey, encodeMainKey,
senderKey = sender ?: SecretKey.new(), senderKey = sender ?: DecryptingSecretKey.new(),
).encoded ).encoded
) )
} }
@@ -274,9 +276,12 @@ sealed class Container {
} }
@Transient @Transient
override var decryptedWithKeyId: KeyId? = null override var decryptedWithKey: UniversalKey? = null
private set private set
override val decryptedWithKeyId: KeyId?
get() = decryptedWithKey?.id
override val keyIds: List<KeyId> = encryptedKeys.map { it.tag } override val keyIds: List<KeyId> = encryptedKeys.map { it.tag }
override fun decryptWith(keyRing: UniversalRing): UByteArray? { override fun decryptWith(keyRing: UniversalRing): UByteArray? {
@@ -292,11 +297,8 @@ sealed class Container {
}.getOrNull()?.let { k -> }.getOrNull()?.let { k ->
if (kotlin.runCatching { decryptedData = k.decrypt(encryptedMessage) }.isFailure) if (kotlin.runCatching { decryptedData = k.decrypt(encryptedMessage) }.isFailure)
throw InvalidContainerException() throw InvalidContainerException()
decryptedWithKeyId = key.id decryptedWithKey = key as UniversalKey
mainKey = k mainKey = k
if( key is SecretKey) {
authorisedByKey = Asymmetric.Message.decode(encryptedKey.cipherData).senderPublicKey
}
} }
if (decryptedData != null) return decryptedData if (decryptedData != null) return decryptedData
} }
@@ -311,6 +313,27 @@ 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 {
/** /**
@@ -358,8 +381,8 @@ sealed class Container {
} }
/** /**
* Add one or more [SecretKey] as sender authority coupled with [PublicKey] as * Add one or more [DecryptingSecretKey] as sender authority coupled with [EncryptingPublicKey] as
* a recipient. This is faster than anonymous usage of [PublicKey] only * a recipient. This is faster than anonymous usage of [EncryptingPublicKey] only
*/ */
fun key(vararg pairs: AsymmetricEncryptionPair) { fun key(vararg pairs: AsymmetricEncryptionPair) {
keyPairs.addAll(pairs) keyPairs.addAll(pairs)
@@ -368,7 +391,7 @@ sealed class Container {
/** /**
* Add one or more public keys as recipients. This is slower than using pairs of sender -> recipient. * Add one or more public keys as recipients. This is slower than using pairs of sender -> recipient.
*/ */
fun key(vararg publicKeys: PublicKey) { fun key(vararg publicKeys: EncryptingPublicKey) {
keyPairs.addAll(publicKeys.map { null to it }) keyPairs.addAll(publicKeys.map { null to it })
} }
@@ -429,7 +452,7 @@ sealed class Container {
Single( Single(
pk.id, pk.encryptMessage( pk.id, pk.encryptMessage(
plainData, plainData,
senderKey = sk ?: SecretKey.new(), senderKey = sk ?: DecryptingSecretKey.new(),
randomFill = fillRange randomFill = fillRange
).encoded, ).encoded,
plainData, plainData,
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import net.sergeych.bintools.CRC import net.sergeych.bintools.CRC
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
@@ -8,7 +18,7 @@ import net.sergeych.crypto2.SymmetricKey.WithNonce
/** /**
* Some key able to perform decrypting. It is not serializable by purpose, as not all such * Some key able to perform decrypting. It is not serializable by purpose, as not all such
* keys are wise to transfer/save. Concrete implementations are, like [SymmetricKey] or * keys are wise to transfer/save. Concrete implementations are, like [SymmetricKey] or
* [SecretKey]. * [DecryptingSecretKey].
*/ */
interface DecryptingKey : NonceBased, KeyInstance { interface DecryptingKey : NonceBased, KeyInstance {
/** /**
@@ -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)
}
}
}
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.util.encodeToUByteArray import com.ionspin.kotlin.crypto.util.encodeToUByteArray
@@ -0,0 +1,78 @@
/*
* 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))
}
@@ -1,9 +1,21 @@
/*
* 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 package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.generichash.GenericHash import com.ionspin.kotlin.crypto.generichash.GenericHash
import com.ionspin.kotlin.crypto.util.encodeToUByteArray import com.ionspin.kotlin.crypto.util.encodeToUByteArray
import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.Flow 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_256
import org.kotlincrypto.hash.sha3.SHA3_384 import org.kotlincrypto.hash.sha3.SHA3_384
@@ -117,27 +129,41 @@ 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 situation where strength is of concern while extending its length above hash block size does not improve * in a 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.1 * not known beforehand.
* *
* 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.encodeToUByteArray() var src = base
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.slice(0..<(sizeInBytes - result.size)) else result += hash.copyOf(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,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.LibsodiumInitializer import com.ionspin.kotlin.crypto.LibsodiumInitializer
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -10,7 +20,7 @@ import kotlinx.serialization.Serializable
* Important. `KeyId` of matching keys are the same, so you can use it to identify * Important. `KeyId` of matching keys are the same, so you can use it to identify
* and find matching keys in the [UniversalRing], etc. For example: * and find matching keys in the [UniversalRing], etc. For example:
* *
* - [SecretKey] and [PublicKey] from the same pair have the same `KeyId`, thus the former * - [DecryptingSecretKey] and [EncryptingPublicKey] from the same pair have the same `KeyId`, thus the former
* can decrypt what was encrypted with the latter. * can decrypt what was encrypted with the latter.
* *
* - [SigningSecretKey] and corresponding [VerifyingKey] have the same `KeyId`. Use it to pick a proper key for * - [SigningSecretKey] and corresponding [VerifyingKey] have the same `KeyId`. Use it to pick a proper key for
@@ -18,7 +28,7 @@ import kotlinx.serialization.Serializable
* *
* See [PBKD.Params.deriveKey] for deriving keys from id. * See [PBKD.Params.deriveKey] for deriving keys from id.
* *
* See [id], and [BinaryId] class for more. Note that for [PublicKey] and [VerifyingPublicKey] [BinaryId.asPublicKey] * See [id], and [BinaryId] class for more. Note that for [EncryptingPublicKey] and [VerifyingPublicKey] [BinaryId.asPublicKey]
* and [BinaryId.asVerifyingKey] restore actual keys, providing [BinaryId.magic] has proper value, see [KeysmagicNumber]] * and [BinaryId.asVerifyingKey] restore actual keys, providing [BinaryId.magic] has proper value, see [KeysmagicNumber]]
* *
* @param id actual id used in equality test amd hash code generation. `Id` of the matching keys is the same. * @param id actual id used in equality test amd hash code generation. `Id` of the matching keys is the same.
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
/** /**
@@ -0,0 +1,65 @@
/*
* 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(),
)
}
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
enum class KeysmagicNumber(val label: String) { enum class KeysmagicNumber(val label: String) {
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
@@ -50,6 +60,12 @@ 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
*/ */
@@ -87,6 +103,10 @@ sealed class Multikey {
} }
return false return false
} }
override fun mentionedKeys(): Set<VerifyingPublicKey> {
return validKeys
}
} }
/** /**
@@ -109,6 +129,10 @@ sealed class Multikey {
} }
return false return false
} }
override fun mentionedKeys(): Set<VerifyingPublicKey> {
return validKeys.flatMap { it.mentionedKeys() }.toSet()
}
} }
/** /**
@@ -119,6 +143,7 @@ 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()
} }
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
interface NonceBased { interface NonceBased {
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
/** /**
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.util.encodeToUByteArray import com.ionspin.kotlin.crypto.util.encodeToUByteArray
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
@@ -7,20 +17,22 @@ import net.sergeych.bipack.decodeFromBipack
import net.sergeych.crypto2.VerifyingPublicKey.Companion.toString import net.sergeych.crypto2.VerifyingPublicKey.Companion.toString
import net.sergeych.mp_tools.decodeBase64Url import net.sergeych.mp_tools.decodeBase64Url
@Deprecated("Use EncryptingPublicKey",ReplaceWith("EncryptingPublicKey"))
typealias PublicKey = EncryptingPublicKey
/** /**
* The public for public-key encryption. It encrypts messages that can only be decrypted with corresponding * The public for public-key encryption. It encrypts messages that can only be decrypted with corresponding
* [SecretKey]. * [DecryptingSecretKey].
*/ */
@Serializable @Serializable
@SerialName("encp") @SerialName("encp")
class PublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingKey { class EncryptingPublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingKey {
override val magic: KeysmagicNumber = KeysmagicNumber.defaultAssymmetric override val magic: KeysmagicNumber = KeysmagicNumber.defaultAssymmetric
@Transient @Transient
override val label: String = "pub" override val label: String = "pub"
/** /**
* Create an anonymous message that could be decrypted only with the [SecretKey] that corresponds this. * Create an anonymous message that could be decrypted only with the [DecryptingSecretKey] that corresponds this.
* Anonymous message uses one-time secret key, the public part of which is included into the * Anonymous message uses one-time secret key, the public part of which is included into the
* [Asymmetric.Message], so the sender could not be identified. * [Asymmetric.Message], so the sender could not be identified.
* *
@@ -57,18 +69,18 @@ class PublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingK
fun encryptMessage( fun encryptMessage(
plainData: UByteArray, plainData: UByteArray,
nonce: UByteArray = randomNonce(), nonce: UByteArray = randomNonce(),
senderKey: SecretKey = newSecretKey(), senderKey: DecryptingSecretKey = newSecretKey(),
randomFill: IntRange? = null, randomFill: IntRange? = null,
) = Asymmetric.createMessage(senderKey, this, WithFill.encode(plainData, randomFill), nonce) ) = Asymmetric.createMessage(senderKey, this, WithFill.encode(plainData, randomFill), nonce)
/** /**
* Encrypt message using the specified secret key as sender authentication. Recipient, the party having * Encrypt message using the specified secret key as sender authentication. Recipient, the party having
* [SecretKey] corresponding to this one, will be able to decrypt the message and be sure that [senderKey] * [DecryptingSecretKey] corresponding to this one, will be able to decrypt the message and be sure that [senderKey]
* was the author and the message was not altered. * was the author and the message was not altered.
*/ */
fun encryptMessage( fun encryptMessage(
plainData: UByteArray, plainData: UByteArray,
senderKey: SecretKey, senderKey: DecryptingSecretKey,
randomFill: IntRange? = null, randomFill: IntRange? = null,
): Asymmetric.Message = ): Asymmetric.Message =
Asymmetric.createMessage(senderKey, this, WithFill.encode(plainData, randomFill)) Asymmetric.createMessage(senderKey, this, WithFill.encode(plainData, randomFill))
@@ -81,12 +93,12 @@ class PublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingK
* possible) * possible)
* @throws IllegalArgumentException the public key isn't recognized * @throws IllegalArgumentException the public key isn't recognized
*/ */
fun parse(text: String): PublicKey { fun parse(text: String): EncryptingPublicKey {
val s = text.trim() val s = text.trim()
fun parseId(t: String): PublicKey{ fun parseId(t: String): EncryptingPublicKey {
val id = BinaryId.restoreFromString(t) val id = BinaryId.restoreFromString(t)
if( id.magic != KeysmagicNumber.defaultAssymmetric.ordinal) if (id.magic != KeysmagicNumber.defaultAssymmetric.ordinal)
throw IllegalArgumentException("invalid magick ${id.magic} for PublicKey") throw IllegalArgumentException("invalid magick ${id.magic} for PublicKey")
return id.asPublicKey return id.asPublicKey
} }
@@ -100,10 +112,10 @@ class PublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingK
// consider it is serialized key in base64 format // consider it is serialized key in base64 format
val data = s.decodeBase64Url().asUByteArray() val data = s.decodeBase64Url().asUByteArray()
if (data.size == 32) if (data.size == 32)
PublicKey(data) EncryptingPublicKey(data)
else { else {
runCatching { data.decodeFromBipack<PublicKey>() }.getOrNull() runCatching { data.decodeFromBipack<EncryptingPublicKey>() }.getOrNull()
?: kotlin.runCatching { data.decodeFromBipack<UniversalKey>() as PublicKey } ?: kotlin.runCatching { data.decodeFromBipack<UniversalKey>() as EncryptingPublicKey }
.getOrElse { throw IllegalArgumentException("can't parse verifying key") } .getOrElse { throw IllegalArgumentException("can't parse verifying key") }
} }
} }
@@ -1,7 +1,25 @@
/*
* 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 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.SerializationException
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
/** /**
@@ -10,7 +28,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 [PublicKey] 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.
* *
@@ -20,16 +38,51 @@ import net.sergeych.crypto2.SafeKeyExchange.SessionKey
* instances as often as performance considerations allow. * instances as often as performance considerations allow.
* - 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 [publicKey] 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.
*/ */
class SafeKeyExchange { @Serializable(with = SafeKeyExchange.SafeKeyExchangeSerializer::class)
private val pair = KeyExchange.keypair() class SafeKeyExchange private constructor(
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
* security level and allow using counters as nonce with no extra precautions. * security level and allow using counters as nonce with no extra precautions.
*/ */
@Serializable @Serializable(with = SafeKeyExchange.SessionKeySerializer::class)
class SessionKey( class SessionKey(
val sendingKey: EncryptingKey, val sendingKey: EncryptingKey,
val receivingKey: DecryptingKey, val receivingKey: DecryptingKey,
@@ -76,13 +129,58 @@ class SafeKeyExchange {
} }
@Serializable
private class PackedSessionKey(
val sendingKey: SymmetricKey,
val receivingKey: SymmetricKey,
val isClient: Boolean,
)
object SessionKeySerializer : KSerializer<SessionKey> {
private val packedSerializer = PackedSessionKey.serializer()
override val descriptor: SerialDescriptor = packedSerializer.descriptor
override fun serialize(encoder: Encoder, value: SessionKey) {
val sendingKey = value.sendingKey as? SymmetricKey
?: throw SerializationException("SessionKey.sendingKey must be SymmetricKey")
val receivingKey = value.receivingKey as? SymmetricKey
?: throw SerializationException("SessionKey.receivingKey must be SymmetricKey")
encoder.encodeSerializableValue(
packedSerializer,
PackedSessionKey(sendingKey, receivingKey, value.isClient)
)
}
override fun deserialize(decoder: Decoder): SessionKey {
val packed = decoder.decodeSerializableValue(packedSerializer)
return SessionKey(packed.sendingKey, packed.receivingKey, packed.isClient)
}
}
/** /**
* 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]
@@ -110,4 +208,4 @@ class SafeKeyExchange {
.let { SessionKey(SymmetricKey(it.sendKey), SymmetricKey(it.receiveKey), isClient = false) } .let { SessionKey(SymmetricKey(it.sendKey), SymmetricKey(it.receiveKey), isClient = false) }
} }
@@ -1,11 +1,22 @@
/*
* 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 package net.sergeych.crypto2
import kotlinx.datetime.Instant import kotlin.time.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
import net.sergeych.crypto2.Seal.Companion.create import net.sergeych.crypto2.Seal.Companion.create
import net.sergeych.utools.now import net.sergeych.utools.now
import kotlin.time.Duration.Companion.seconds
/** /**
* Extended public-key signature. * Extended public-key signature.
@@ -67,7 +78,7 @@ class Seal(
*/ */
fun verify(message: UByteArray) { fun verify(message: UByteArray) {
val n = now() val n = now()
if (createdAt > n) throw IllegalSignatureException("signature's timestamp in the future") if (createdAt - 45.seconds > n) throw IllegalSignatureException("signature's timestamp in the future: $createdAt / $n")
expiresAt?.let { expiresAt?.let {
if (n >= it) throw ExpiredSignatureException("signature expired at $it") if (n >= it) throw ExpiredSignatureException("signature expired at $it")
} }
@@ -98,7 +109,7 @@ class Seal(
* to check the authenticity of the arbitrary [message] using a public key, [VerifyingPublicKey] * to check the authenticity of the arbitrary [message] using a public key, [VerifyingPublicKey]
* instance, using public-key signing algorithms. * instance, using public-key signing algorithms.
* *
* Unlike a regular binary signature, Seal contains the signer's [publicKey], and also * Unlike a regular binary signature, Seal contains the signer's [EncryptingPublicKey], and also
* [createdAt] and [expiresAt] fields which are also signed and are guaranteed to be non-tampered * [createdAt] and [expiresAt] fields which are also signed and are guaranteed to be non-tampered
* if the [isValid] returns true (or [verify] does not throw). See [isExpired]. * if the [isValid] returns true (or [verify] does not throw). See [isExpired].
* *
@@ -1,6 +1,16 @@
/*
* 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 package net.sergeych.crypto2
import kotlinx.datetime.Instant import kotlin.time.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
@@ -82,6 +92,7 @@ class SealedBox(
/** /**
* Unpack bipack-encoded payload * Unpack bipack-encoded payload
*/ */
@Suppress("unused")
inline fun <reified T>unpack(): T = BipackDecoder.decode(message) inline fun <reified T>unpack(): T = BipackDecoder.decode(message)
init { init {
@@ -1,29 +1,44 @@
/*
* 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 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
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
@Deprecated("Use DecryptingSecretKey instead",ReplaceWith("DecryptingSecretKey"))
typealias SecretKey = DecryptingSecretKey
/** /**
* The secret key used in public-key encryption; it is used to _decrypt_ data encrypted with its * The secret key used in public-key encryption; it is used to _decrypt_ data encrypted with its
* public counterpart, see [publicKey]. * public counterpart, see [EncryptingPublicKey].
*/ */
@Serializable @Serializable
@SerialName("encs") @SerialName("encs")
class SecretKey( class DecryptingSecretKey(
override val keyBytes: UByteArray, override val keyBytes: UByteArray,
@Transient @Transient
val _cachedPublicKey: PublicKey? = null, val _cachedPublicKey: EncryptingPublicKey? = null,
) : DecryptingKey, UniversalKey() { ) : DecryptingKey, UniversalKey() {
@Transient @Transient
override val label: String = "sec" override val label: String = "sec"
/** /**
* Decrypt with authentication checks the message which must have [Asymmetric.Message.senderPublicKey] set. * Decrypt with authentication checks the message which must have [Asymmetric.Message.senderPublicKey] set.
* Use [decryptWithSenderKey] otherwise. Note that the authenticated encryption is always use, even if * Use [decryptWithSenderKey] otherwise. Note that the authenticated encryption is always use, even if
* the [PublicKey.encryptAnonymousMessage] was used to create a message, if it is successfully decrypted, * the [EncryptingPublicKey.encryptAnonymousMessage] was used to create a message, if it is successfully decrypted,
* it is guaranteed that the message was not altered after creation. * it is guaranteed that the message was not altered after creation.
* *
* @throws DecryptionFailedException If the message is tampered (changed after creation) or was not intended for us, * @throws DecryptionFailedException If the message is tampered (changed after creation) or was not intended for us,
@@ -35,20 +50,20 @@ class SecretKey(
* Decrypt using [senderPublicKey] as a sender key (overriding the [Asymmetric.Message.senderPublicKey] if set). * Decrypt using [senderPublicKey] as a sender key (overriding the [Asymmetric.Message.senderPublicKey] if set).
* See [decrypt] for more. * See [decrypt] for more.
*/ */
fun decryptWithSenderKey(message: Asymmetric.Message, senderPublicKey: PublicKey): UByteArray = fun decryptWithSenderKey(message: Asymmetric.Message, senderPublicKey: EncryptingPublicKey): UByteArray =
message.decryptWithSenderKey(senderPublicKey, this) message.decryptWithSenderKey(senderPublicKey, this)
@Transient @Transient
private var cachedPublicKey: PublicKey? = _cachedPublicKey private var cachedPublicKey: EncryptingPublicKey? = _cachedPublicKey
/** /**
* The corresponding public key * The corresponding public key
*/ */
val publicKey: PublicKey by lazy { val publicKey: EncryptingPublicKey by lazy {
if (cachedPublicKey != null) if (cachedPublicKey != null)
cachedPublicKey!! cachedPublicKey!!
else else
PublicKey(ScalarMultiplication.scalarMultiplicationBase(keyBytes)) EncryptingPublicKey(ScalarMultiplication.scalarMultiplicationBase(keyBytes))
.also { cachedPublicKey = it } .also { cachedPublicKey = it }
} }
@@ -71,18 +86,46 @@ class SecretKey(
get() = 0 get() = 0
companion object { companion object {
data class KeyPair(val secretKey: SecretKey, val publicKey: PublicKey) 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.
*/ */
fun generateKeys(): KeyPair { fun generateKeys(): KeyPair {
val p = Box.keypair() val p = Box.keypair()
val pk = PublicKey(p.publicKey) val pk = EncryptingPublicKey(p.publicKey)
return KeyPair(SecretKey(p.secretKey, pk), pk) return KeyPair(DecryptingSecretKey(p.secretKey, pk), pk)
} }
fun new(): SecretKey = generateKeys().secretKey /**
* 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
/**
* 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")
} }
} }
@@ -1,6 +1,16 @@
/*
* 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 package net.sergeych.crypto2
import kotlinx.datetime.Instant import kotlin.time.Instant
interface SigningKey: KeyInstance { interface SigningKey: KeyInstance {
val verifyingKey: VerifyingPublicKey val verifyingKey: VerifyingPublicKey
@@ -1,7 +1,18 @@
/*
* 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 package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.signature.Signature import com.ionspin.kotlin.crypto.signature.Signature
import kotlinx.datetime.Instant import com.ionspin.kotlin.crypto.signature.crypto_sign_SEEDBYTES
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
@@ -66,13 +77,41 @@ 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")
} }
} }
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.secretbox.SecretBox import com.ionspin.kotlin.crypto.secretbox.SecretBox
@@ -36,13 +46,54 @@ 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 {
@@ -54,6 +105,22 @@ 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
@@ -75,4 +142,4 @@ class SymmetricKey(
val keyLength = crypto_secretbox_KEYBYTES val keyLength = crypto_secretbox_KEYBYTES
} }
} }
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -17,7 +27,7 @@ sealed class UniversalKey : KeyInstance {
/** /**
* Key ID positively identify key from the point of view of _decrypting or verifying_. So matching [VerifyingKey] * Key ID positively identify key from the point of view of _decrypting or verifying_. So matching [VerifyingKey]
* and [SigningKey] will have the same id, same as matching [PublicKey] and [SecretKey]. * and [SigningKey] will have the same id, same as matching [EncryptingPublicKey] and [DecryptingSecretKey].
* *
* KeyId is based on [BinaryId] which includes checksum (crc8) and magick number for additional security, * KeyId is based on [BinaryId] which includes checksum (crc8) and magick number for additional security,
* see [KeysmagicNumber]. * see [KeysmagicNumber].
@@ -43,7 +53,7 @@ sealed class UniversalKey : KeyInstance {
} }
companion object { companion object {
fun newSecretKey() = SecretKey.new() fun newSecretKey() = DecryptingSecretKey.new()
fun newSigningKey() = SigningSecretKey.new() fun newSigningKey() = SigningSecretKey.new()
@Suppress("unused") @Suppress("unused")
@@ -57,7 +67,7 @@ sealed class UniversalKey : KeyInstance {
val s = text.trim() val s = text.trim()
return when { return when {
s.startsWith("\uD83D\uDDDDpub#") || s.startsWith("pub#") -> s.startsWith("\uD83D\uDDDDpub#") || s.startsWith("pub#") ->
PublicKey.parse(s) EncryptingPublicKey.parse(s)
s.startsWith("\uD83D\uDDDDver#") || s.startsWith("ver#") -> s.startsWith("\uD83D\uDDDDver#") || s.startsWith("ver#") ->
VerifyingPublicKey.parse(s) VerifyingPublicKey.parse(s)
else -> { else -> {
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
@@ -14,7 +24,7 @@ import kotlinx.serialization.Transient
@SerialName("uprv") @SerialName("uprv")
class UniversalPrivateKey( class UniversalPrivateKey(
val signingKey: SigningSecretKey, val signingKey: SigningSecretKey,
val decryptingKey: SecretKey val decryptingKey: DecryptingSecretKey
) : UniversalKey(), DecryptingKey by decryptingKey, SigningKey by signingKey { ) : UniversalKey(), DecryptingKey by decryptingKey, SigningKey by signingKey {
override val keyBytes by lazy { signingKey.keyBytes + decryptingKey.keyBytes } override val keyBytes by lazy { signingKey.keyBytes + decryptingKey.keyBytes }
@@ -38,10 +48,38 @@ 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(), SecretKey.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")
} }
} }
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
@@ -15,7 +25,7 @@ import kotlinx.serialization.Transient
@SerialName("upub") @SerialName("upub")
class UniversalPublicKey( class UniversalPublicKey(
val verifyingKey: VerifyingPublicKey, val verifyingKey: VerifyingPublicKey,
val encryptingKey: PublicKey val encryptingKey: EncryptingPublicKey
): UniversalKey(), VerifyingKey by verifyingKey, EncryptingKey by encryptingKey{ ): UniversalKey(), VerifyingKey by verifyingKey, EncryptingKey by encryptingKey{
override val keyBytes by lazy { verifyingKey.keyBytes + encryptingKey.keyBytes } override val keyBytes by lazy { verifyingKey.keyBytes + encryptingKey.keyBytes }
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -38,7 +48,7 @@ class UniversalRing(
/** /**
* Find a key of the specified type that matches the id. In general, you require key implementations like * Find a key of the specified type that matches the id. In general, you require key implementations like
* [SecretKey], [PublicKey], [VerifyingPublicKey], [SigningSecretKey] and [SymmetricKey], * [DecryptingSecretKey], [EncryptingPublicKey], [VerifyingPublicKey], [SigningSecretKey] and [SymmetricKey],
* or just key interfaces: [EncryptingKey], [DecryptingKey], [SigningKey] and [VerifyingKey]. * or just key interfaces: [EncryptingKey], [DecryptingKey], [SigningKey] and [VerifyingKey].
* *
* Note that key interfaces are not serializable as for now, you should try to cast to a serializable * Note that key interfaces are not serializable as for now, you should try to cast to a serializable
@@ -219,6 +229,8 @@ 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
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
interface VerifyingKey: KeyInstance { interface VerifyingKey: KeyInstance {
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.signature.InvalidSignatureException import com.ionspin.kotlin.crypto.signature.InvalidSignatureException
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
/** /**
+141 -11
View File
@@ -1,3 +1,13 @@
/*
* 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 package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.pwhash.* import com.ionspin.kotlin.crypto.pwhash.*
@@ -32,15 +42,28 @@ 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 * Random salt of proper size is used by default. If a custom [salt] size is not [Argon.saltSize],
* it is deterministically normalized with `Hash.Blake2b.deriveSaltFormBytes(...)`.
*/ */
fun kdfForSize(numberOfKeys: Int): KDF = creteDefault(SymmetricKey.keyLength * numberOfKeys, this) fun kdfForSize(numberOfKeys: Int, salt: UByteArray = Argon.randomSalt()): KDF =
createDefault(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].
* Random salt of proper size is used * If the [salt] size is not [Argon.saltSize], it is deterministically normalized with
* `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_.
@@ -48,13 +71,22 @@ sealed class KDF {
* to change with time. * to change with time.
*/ */
@Suppress("unused") @Suppress("unused")
fun deriveMultiple(password: String, count: Int): List<SymmetricKey> = fun deriveMultiple(password: String, count: Int, salt: UByteArray): List<SymmetricKey> =
kdfForSize(count).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): SymmetricKey = deriveMultiple(password, 1).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)
} }
/** /**
@@ -62,6 +94,11 @@ 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.
@@ -120,6 +157,17 @@ 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
@@ -160,7 +208,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 $keySize bytes" } require(keySize >= minKeySize) { "The key size should be at least $minKeySize bytes" }
return when (complexity) { return when (complexity) {
FixedLow -> Argon( FixedLow -> Argon(
V2id_13, V2id_13,
@@ -209,14 +257,96 @@ sealed class KDF {
} }
} }
companion object { /**
fun creteDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF { * Fast HKDF derivation for already strong byte material, implemented as described in
return Argon.create(complexity, salt, keySize) * [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 {
/**
* 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)
} }
@@ -1,3 +1,13 @@
/*
* 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.
*/
@file:Suppress("unused") @file:Suppress("unused")
package net.sergeych.crypto2 package net.sergeych.crypto2
@@ -1,3 +1,13 @@
/*
* 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.
*/
@file:Suppress("unused") @file:Suppress("unused")
package net.sergeych.crypto2 package net.sergeych.crypto2
@@ -1,3 +1,13 @@
/*
* 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.tools package net.sergeych.tools
@Suppress("unused") @Suppress("unused")
@@ -1,3 +1,13 @@
/*
* 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.tools package net.sergeych.tools
import net.sergeych.synctools.ProtectedOp import net.sergeych.synctools.ProtectedOp
@@ -5,7 +15,7 @@ import net.sergeych.synctools.invoke
/** /**
* Multiplatform (JS and battery included) atomically mutable value. * Multiplatform (JS and battery included) atomically mutable value.
* Actual value can be either changed in a block of [mutuate] when * Actual value can be either changed in a block of [mutate] when
* new value _depends on the current value_ or use a same [value] * new value _depends on the current value_ or use a same [value]
* property that is thread-safe where there are threads and just safe * property that is thread-safe where there are threads and just safe
* otherwise ;) * otherwise ;)
@@ -1,3 +1,13 @@
/*
* 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.
*/
@file:Suppress("unused") @file:Suppress("unused")
package net.sergeych.tools package net.sergeych.tools
@@ -1,3 +1,13 @@
/*
* 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.tools package net.sergeych.tools
import net.sergeych.bipack.BipackDecoder import net.sergeych.bipack.BipackDecoder
@@ -1,3 +1,13 @@
/*
* 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.
*/
@file:Suppress("unused") @file:Suppress("unused")
package net.sergeych.utools package net.sergeych.utools
@@ -1,3 +1,13 @@
/*
* 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.utools package net.sergeych.utools
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
@@ -1,9 +1,19 @@
/*
* 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.
*/
@file:Suppress("unused") @file:Suppress("unused")
package net.sergeych.utools package net.sergeych.utools
import kotlinx.datetime.Clock import kotlin.time.Clock
import kotlinx.datetime.Instant import kotlin.time.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()
@@ -1,3 +1,13 @@
/*
* 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 org.komputing.khash.keccak package org.komputing.khash.keccak
import com.ionspin.kotlin.bignum.integer.BigInteger import com.ionspin.kotlin.bignum.integer.BigInteger
@@ -1,3 +1,13 @@
/*
* 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.
*/
@file:Suppress("unused") @file:Suppress("unused")
package org.komputing.khash.keccak package org.komputing.khash.keccak
@@ -1,3 +1,13 @@
/*
* 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 org.komputing.khash.keccak.extensions package org.komputing.khash.keccak.extensions
/** /**
@@ -1,3 +1,13 @@
/*
* 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.
*/
@file:Suppress("unused") @file:Suppress("unused")
package org.komputing.khash.keccak.extensions package org.komputing.khash.keccak.extensions
+28
View File
@@ -1,5 +1,21 @@
/*
* 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.
*/
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.BinaryId
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
class BinaryIdTest { class BinaryIdTest {
@@ -13,4 +29,16 @@ class BinaryIdTest {
assertEquals(4, a.id.size) assertEquals(4, a.id.size)
} }
@Test
fun testByteChunkSizes() = runTest {
initCrypto()
val x = ByteChunk.random(3)
assertEquals(3, x.data.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.asByteArray), BipackEncoder.encode(x))
}
} }
+15 -3
View File
@@ -1,3 +1,13 @@
/*
* 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.
*/
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.encodeToString
@@ -24,6 +34,8 @@ class ContainerTest {
assertNotNull(d) assertNotNull(d)
assertContentEquals(data, d) assertContentEquals(data, d)
assertTrue { c1.isDecrypted } assertTrue { c1.isDecrypted }
assertEquals(syk1.id, c1.decryptedWithKeyId)
assertEquals(syk1, c1.decryptedWithKey)
val data2 = "To push unpushinable".encodeToUByteArray() val data2 = "To push unpushinable".encodeToUByteArray()
val c2 = Container.decode(c.updateData(data2).encoded) val c2 = Container.decode(c.updateData(data2).encoded)
@@ -51,7 +63,7 @@ class ContainerTest {
assertContentEquals(data, d) assertContentEquals(data, d)
assertTrue { c1.isDecrypted } assertTrue { c1.isDecrypted }
assertEquals(p2.publicKey.id, c1.decryptedWithKeyId) assertEquals(p2.publicKey.id, c1.decryptedWithKeyId)
assertEquals(p1.publicKey, c1.authorisedByKey) assertEquals(p2.secretKey, c1.decryptedWithKey)
val data2 = "To push unpushinable".encodeToUByteArray() val data2 = "To push unpushinable".encodeToUByteArray()
val c2 = Container.decode(c.updateData(data2).encoded) val c2 = Container.decode(c.updateData(data2).encoded)
@@ -78,7 +90,7 @@ class ContainerTest {
assertContentEquals(data, d) assertContentEquals(data, d)
assertTrue { c1.isDecrypted } assertTrue { c1.isDecrypted }
assertEquals(p4.publicKey.id, c1.decryptedWithKeyId) assertEquals(p4.publicKey.id, c1.decryptedWithKeyId)
assertEquals(p1.publicKey, c1.authorisedByKey) assertEquals(p4.secretKey, c1.decryptedWithKey)
val data2 = "To push unpushinable".encodeToUByteArray() val data2 = "To push unpushinable".encodeToUByteArray()
val c2 = Container.decode(c.updateData(data2).encoded) val c2 = Container.decode(c.updateData(data2).encoded)
@@ -328,4 +340,4 @@ class ContainerTest {
expectOpen(data2, syk3, p3.secretKey, p4.secretKey) expectOpen(data2, syk3, p3.secretKey, p4.secretKey)
} }
} }
+48 -19
View File
@@ -1,24 +1,26 @@
/*
* 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.
*/
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Clock import kotlin.time.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() {
@@ -53,17 +55,44 @@ class HashTest {
@Test @Test
fun deriveSaltTest() = runTest { fun deriveSaltTest() = runTest {
initCrypto() initCrypto()
for( i in 2..257 ) { for (hash in Hash.entries) {
val x = Hash.Sha3AndBlake.deriveSalt("base one", i) for (i in 1..257) {
val y = Hash.Sha3AndBlake.deriveSalt("base one", i) val x = hash.deriveSalt("base one", i)
val z = Hash.Sha3AndBlake.deriveSalt("base two", i) val y = hash.deriveSalt("base one", i)
assertContentEquals(x, y) val z = hash.deriveSalt("base two", i)
assertFalse { x contentEquals z } assertContentEquals(x, y)
assertEquals(x.size, i) if (i > 1) assertFalse { x contentEquals z }
assertEquals(y.size, i) assertEquals(i, x.size)
assertEquals(z.size, i) assertEquals(i, y.size)
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
}
+170 -1
View File
@@ -1,9 +1,23 @@
/*
* 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.
*/
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
@@ -45,4 +59,159 @@ 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()
}
}
+141 -10
View File
@@ -1,7 +1,16 @@
/*
* 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.
*/
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
@@ -73,6 +82,48 @@ 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()
@@ -108,6 +159,44 @@ 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()
@@ -132,6 +221,48 @@ 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
fun sessionKeySerializationTest() = runTest {
initCrypto()
val ske = SafeKeyExchange()
val cke = SafeKeyExchange()
val clientSessionKey = cke.clientSessionKey(ske.publicKey)
val serverSessionKey = ske.serverSessionKey(cke.publicKey)
val restoredClientSessionKey = unpack<SafeKeyExchange.SessionKey>(pack(clientSessionKey))
val restoredServerSessionKey = unpack<SafeKeyExchange.SessionKey>(pack(serverSessionKey))
assertEquals(clientSessionKey, restoredClientSessionKey)
assertEquals(serverSessionKey, restoredServerSessionKey)
assertContentEquals(clientSessionKey.sessionTag, restoredClientSessionKey.sessionTag)
assertContentEquals(serverSessionKey.sessionTag, restoredServerSessionKey.sessionTag)
val src = "Hello from restored session keys!"
assertEquals(src, restoredServerSessionKey.decryptString(restoredClientSessionKey.encrypt(src)))
assertEquals(src, restoredClientSessionKey.decryptString(restoredServerSessionKey.encrypt(src)))
}
@Test @Test
fun asymmetricKeyTest() = runTest { fun asymmetricKeyTest() = runTest {
initCrypto() initCrypto()
@@ -177,7 +308,7 @@ class KeysTest {
// println(sk0.publicKey) // println(sk0.publicKey)
val j = Json { prettyPrint = true } val j = Json { prettyPrint = true }
val sk1 = j.decodeFromString<SecretKey>(j.encodeToString(sk0)) val sk1 = j.decodeFromString<DecryptingSecretKey>(j.encodeToString(sk0))
assertEquals(sk0, sk1) assertEquals(sk0, sk1)
assertEquals(pk0, sk1.publicKey) assertEquals(pk0, sk1.publicKey)
// println(j.encodeToString(sk1)) // println(j.encodeToString(sk1))
@@ -206,9 +337,9 @@ class KeysTest {
assertEquals(usy2, usy1) assertEquals(usy2, usy1)
assertFalse { usy1 == usy3 } assertFalse { usy1 == usy3 }
val sk1 = SecretKey.new() val sk1 = DecryptingSecretKey.new()
val sk2 = SecretKey(sk1.keyBytes) val sk2 = DecryptingSecretKey(sk1.keyBytes)
val sk3 = SecretKey.new() val sk3 = DecryptingSecretKey.new()
assertEquals(sk1, sk2) assertEquals(sk1, sk2)
assertEquals(sk2, sk1) assertEquals(sk2, sk1)
@@ -408,13 +539,13 @@ class KeysTest {
@Test @Test
fun testEncodedSizes2() = runTest { fun testEncodedSizes2() = runTest {
initCrypto() initCrypto()
val x = SecretKey.new() val x = DecryptingSecretKey.new()
// println("key bytes: ${x.keyBytes.size}:\n${x.keyBytes.toDump()}") // println("key bytes: ${x.keyBytes.size}:\n${x.keyBytes.toDump()}")
val y = BipackEncoder.encode(x) val y = BipackEncoder.encode(x)
// println("packed: ${y.size}: ${y.toDump()}") // println("packed: ${y.size}: ${y.toDump()}")
assertTrue { x.keyBytes.size + 5 > y.size } assertTrue { x.keyBytes.size + 5 > y.size }
assertEquals(x, BipackDecoder.decode<SecretKey>(y)) assertEquals(x, BipackDecoder.decode<DecryptingSecretKey>(y))
assertContentEquals(x.keyBytes, BipackDecoder.decode<SecretKey>(y).keyBytes) assertContentEquals(x.keyBytes, BipackDecoder.decode<DecryptingSecretKey>(y).keyBytes)
} }
@Test @Test
@@ -422,7 +553,7 @@ class KeysTest {
initCrypto() initCrypto()
val k1 = SigningSecretKey.new() val k1 = SigningSecretKey.new()
val k2 = k1.verifyingKey val k2 = k1.verifyingKey
val k3 = SecretKey.new() val k3 = DecryptingSecretKey.new()
val k4 = k3.publicKey val k4 = k3.publicKey
val k5 = UniversalPrivateKey.new() val k5 = UniversalPrivateKey.new()
@@ -432,7 +563,7 @@ class KeysTest {
assertContentEquals(k2.keyBytes, k2.id.id.body) assertContentEquals(k2.keyBytes, k2.id.id.body)
val k7 = SymmetricKey.new() 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) { fun testToString(k: UniversalKey) {
val s = k.toString() val s = k.toString()
+10
View File
@@ -1,3 +1,13 @@
/*
* 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.
*/
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.crypto2.PBKD import net.sergeych.crypto2.PBKD
import net.sergeych.crypto2.initCrypto import net.sergeych.crypto2.initCrypto
+11 -1
View File
@@ -1,5 +1,15 @@
/*
* 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.
*/
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant import kotlin.time.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
+13 -3
View File
@@ -1,3 +1,13 @@
/*
* 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.
*/
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 net.sergeych.bintools.toDump import net.sergeych.bintools.toDump
@@ -17,7 +27,7 @@ class RingTest {
assertEquals(y1, y2) assertEquals(y1, y2)
val e1 = Asymmetric.newSecretKey() val e1 = Asymmetric.newSecretKey()
val e2: SecretKey = BipackDecoder.decode(BipackEncoder.encode(e1)) val e2: DecryptingSecretKey = BipackDecoder.decode(BipackEncoder.encode(e1))
assertEquals(e1, e2) assertEquals(e1, e2)
val k1 = SymmetricKey("1234567890Hello,dolly.here-we-go".encodeToUByteArray()) as UniversalKey val k1 = SymmetricKey("1234567890Hello,dolly.here-we-go".encodeToUByteArray()) as UniversalKey
@@ -154,14 +164,14 @@ class RingTest {
var r1 = ra + rb + rc + rd var r1 = ra + rb + rc + rd
assertEquals(a, r1.findKey<SecretKey>(a.id)) assertEquals(a, r1.findKey<DecryptingSecretKey>(a.id))
assertEquals(a, r1.keyByTag<UniversalKey>("foo_a")) assertEquals(a, r1.keyByTag<UniversalKey>("foo_a"))
assertEquals(b, r1.findKey<SigningKey>(b.id)) assertEquals(b, r1.findKey<SigningKey>(b.id))
assertEquals(c, r1.findById(c.id).first()) assertEquals(c, r1.findById(c.id).first())
r1 = UniversalRing.join(listOf(ra, rb, rc, rd)) r1 = UniversalRing.join(listOf(ra, rb, rc, rd))
assertEquals(a, r1.findKey<SecretKey>(a.id)) assertEquals(a, r1.findKey<DecryptingSecretKey>(a.id))
assertEquals(a, r1.keyByTag<UniversalKey>("foo_a")) assertEquals(a, r1.keyByTag<UniversalKey>("foo_a"))
assertEquals(b, r1.findKey<SigningKey>(b.id)) assertEquals(b, r1.findKey<SigningKey>(b.id))
assertEquals(c, r1.findById(c.id).first()) assertEquals(c, r1.findById(c.id).first())
+87
View File
@@ -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()}")
}
+10
View File
@@ -1,3 +1,13 @@
/*
* 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.
*/
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.bintools.encodeToHex import net.sergeych.bintools.encodeToHex
import net.sergeych.crypto2.BinaryId import net.sergeych.crypto2.BinaryId
+10
View File
@@ -1,3 +1,13 @@
/*
* 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.
*/
import kotlin.test.fail import kotlin.test.fail
inline fun <reified T: Throwable>assertThrows(f: ()->Unit): T { inline fun <reified T: Throwable>assertThrows(f: ()->Unit): T {
+10
View File
@@ -1,3 +1,13 @@
/*
* 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.
*/
import net.sergeych.bipack.BipackDecoder import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder import net.sergeych.bipack.BipackEncoder