Compare commits
39
Commits
db7453fbb2
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66254f8fa6 | ||
|
|
84a3e82216 | ||
|
|
d402afe1bd | ||
|
|
ff494ab4ef | ||
|
|
2d55af4e3b | ||
|
|
a77fdd79c1 | ||
|
|
d6b171229f | ||
|
|
cd57627cdb | ||
|
|
5c3ff72123 | ||
|
|
d0f1fffe6d | ||
|
|
8015a4310b | ||
|
|
13dff8d760 | ||
|
|
3bd06ac7ff | ||
|
|
ca9ab0f7a0 | ||
|
|
b86ac6f00b | ||
|
|
9d338d2f13 | ||
|
|
5228be33ee | ||
|
|
528439f61d | ||
|
|
7c97a843e7 | ||
|
|
1a81cd5110 | ||
|
|
7d3e396cf7 | ||
|
|
85b13ed8ca | ||
|
|
fbbe4d3a34 | ||
|
|
875c0f7a50 | ||
|
|
fe6190eb8d | ||
|
|
776f4e75ff | ||
|
|
fa7263b0e7 | ||
|
|
bd81f88dd8 | ||
|
|
6fcf7841a7 | ||
|
|
4748ea0d65 | ||
|
|
e2d4fb07ad | ||
|
|
d180da309b | ||
|
|
c9e3c57ee2 | ||
|
|
81e02ac88e | ||
|
|
e8d6b2fc02 | ||
|
|
277dc62553 | ||
|
|
7e52a72c6a | ||
|
|
7fa3ab1ca8 | ||
|
|
242cc7d0f5 |
No files matched your search
@@ -9,6 +9,7 @@ build/
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
Generated
-7
@@ -1,10 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
/artifacts/crypto2_js_0_1_0_SNAPSHOT.xml
|
||||
/artifacts/crypto2_jvm_0_1_0_SNAPSHOT.xml
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="crypto2-js-0.1.1-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="crypto2-js-0.1.1-SNAPSHOT.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="crypto2-js-1.0-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="crypto2-js-1.0-SNAPSHOT.jar">
|
||||
<element id="module-output" name="crypto2.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="crypto2-jvm-0.1.1-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="crypto2-jvm-0.1.1-SNAPSHOT.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="crypto2-jvm-1.0-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="crypto2-jvm-1.0-SNAPSHOT.jar">
|
||||
<element id="module-output" name="crypto2.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
@@ -1,8 +0,0 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="crypto2-wasm-js-0.1.1-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="crypto2-wasm-js-0.1.1-SNAPSHOT.jar">
|
||||
<element id="module-output" name="crypto2.wasmJsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+24
@@ -1,5 +1,29 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<DBN-PSQL>
|
||||
<case-options enabled="true">
|
||||
<option name="KEYWORD_CASE" value="lower" />
|
||||
<option name="FUNCTION_CASE" value="lower" />
|
||||
<option name="PARAMETER_CASE" value="lower" />
|
||||
<option name="DATATYPE_CASE" value="lower" />
|
||||
<option name="OBJECT_CASE" value="preserve" />
|
||||
</case-options>
|
||||
<formatting-settings enabled="false" />
|
||||
</DBN-PSQL>
|
||||
<DBN-SQL>
|
||||
<case-options enabled="true">
|
||||
<option name="KEYWORD_CASE" value="lower" />
|
||||
<option name="FUNCTION_CASE" value="lower" />
|
||||
<option name="PARAMETER_CASE" value="lower" />
|
||||
<option name="DATATYPE_CASE" value="lower" />
|
||||
<option name="OBJECT_CASE" value="preserve" />
|
||||
</case-options>
|
||||
<formatting-settings enabled="false">
|
||||
<option name="STATEMENT_SPACING" value="one_line" />
|
||||
<option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
|
||||
<option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
|
||||
</formatting-settings>
|
||||
</DBN-SQL>
|
||||
<ScalaCodeStyleSettings>
|
||||
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
|
||||
</ScalaCodeStyleSettings>
|
||||
|
||||
Generated
+2
@@ -5,6 +5,7 @@
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="/usr/local/Cellar/gradle/7.6/libexec" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
@@ -12,5 +13,6 @@
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
<option name="parallelModelFetch" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ReplaceUntilWithRangeUntil" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="StructuralWrap" enabled="false" level="TYPO" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.20" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+2
-1
@@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17 (5)" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="corretto-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -2,6 +2,16 @@
|
||||
|
||||
Kotlin Multiplatform cryptographic primitives using modern strong cryptography.
|
||||
|
||||
## v0.9.0 for kotlin 2.2.21, new kotlin time compatible
|
||||
|
||||
The primary goal was to fix kotlin-caused incompatibilities with kotlinx.datetime.Instant and Clock; the upgrade shoud be in-place
|
||||
replacement providing calling code ises `kotlin.time.Instant` and
|
||||
`kotlin.time.Clock` respectively. No other changes are needed.
|
||||
|
||||
Also we start to add small syntax sugar methods.
|
||||
|
||||
## v.0.8.4 is built for all platform, IOS and wasmJS included
|
||||
|
||||
Cryptographic API works exactly the same and compiles to any platform supported listed below with no change in source code.
|
||||
|
||||
All primitives meant to send over the network or store are `kotlinx.serialization` compatible, serializers included.
|
||||
@@ -21,7 +31,7 @@ repositories {
|
||||
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
||||
}
|
||||
dependencies {
|
||||
import("net.sergeych:crypto2:0.7.1-SNAPSHOT")
|
||||
import("net.sergeych:crypto2:0.8.4")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -36,6 +46,10 @@ Please see the current documentation [here](https://code.sergeych.net/docs/crypt
|
||||
- All moder browsers, including mobile
|
||||
- Node.js
|
||||
|
||||
## WasmJs
|
||||
|
||||
- All moder browsers, including mobile
|
||||
|
||||
## JVM
|
||||
|
||||
- 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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
- 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
@@ -1,4 +1,14 @@
|
||||
#!/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
|
||||
./gradlew dokkaHtml
|
||||
rsync -avz ./build/dokka/* code.sergeych.net:/bigstore/sergeych_pub/code/docs/crypto2
|
||||
+38
-21
@@ -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.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform") version "2.0.20"
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20"
|
||||
kotlin("multiplatform") version "2.4.0"
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.4.0"
|
||||
id("org.jetbrains.dokka") version "1.9.20"
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "0.7.1-SNAPSHOT"
|
||||
version = "0.9.4-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://maven.universablockchain.com/")
|
||||
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
||||
maven("https://gitea.sergeych.net/api/packages/YoungBlood/maven")
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm {
|
||||
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_11
|
||||
}
|
||||
}
|
||||
jvmToolchain(21)
|
||||
jvm()
|
||||
js {
|
||||
browser()
|
||||
nodejs()
|
||||
@@ -38,36 +46,37 @@ kotlin {
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
mingwX64()
|
||||
// @OptIn(ExperimentalWasmDsl::class)
|
||||
// wasmJs() //no libsodium bindings yet (strangely)
|
||||
// val ktor_version = "2.3.6"
|
||||
@OptIn(ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
all {
|
||||
languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi")
|
||||
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
|
||||
languageSettings.optIn("kotlin.time.ExperimentalTime")
|
||||
}
|
||||
|
||||
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||
|
||||
implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.2")
|
||||
implementation("net.sergeych:multiplatform-crypto-libsodium-bindings:0.9.6")
|
||||
implementation(project.dependencies.platform("org.kotlincrypto.hash:bom:0.5.1"))
|
||||
implementation("org.kotlincrypto.hash:sha3")
|
||||
api("com.ionspin.kotlin:bignum:0.3.9")
|
||||
api("net.sergeych:mp_bintools:0.1.7")
|
||||
api("net.sergeych:mp_stools:1.5.1")
|
||||
api("com.ionspin.kotlin:bignum:0.3.10")
|
||||
api("net.sergeych:mp_bintools:0.3.2")
|
||||
|
||||
}
|
||||
}
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
implementation("org.slf4j:slf4j-simple:2.0.9")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
|
||||
}
|
||||
}
|
||||
val native by creating {
|
||||
@@ -80,7 +89,7 @@ kotlin {
|
||||
}
|
||||
}
|
||||
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) }
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
outputDirectory.set(buildDir.resolve("dokka"))
|
||||
dokkaSourceSets {
|
||||
|
||||
@@ -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
|
||||
org.gradle.parallel=true
|
||||
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.caching=true
|
||||
+11
-1
@@ -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
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -1,19 +1,13 @@
|
||||
#!/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 not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# You may use, distribute and modify this code under the
|
||||
# terms of the private license, which you must obtain from the author
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# 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.
|
||||
# To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
||||
# real dot sergeych at gmail.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
|
||||
@@ -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 {
|
||||
repositories {
|
||||
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
|
||||
|
||||
import com.ionspin.kotlin.crypto.box.Box
|
||||
@@ -14,10 +24,10 @@ import net.sergeych.crypto2.Asymmetric.generateKeys
|
||||
*
|
||||
* ## 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.
|
||||
* - [PublicKey] provides encryption, anonymous or authenticated.
|
||||
* - [SecretKey] provides authenticated decryption of what [PublicKey] was encrypted with.
|
||||
* - [EncryptingPublicKey] provides encryption, anonymous or authenticated.
|
||||
* - [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.
|
||||
*
|
||||
* __Algorithms:__
|
||||
@@ -31,8 +41,8 @@ object Asymmetric {
|
||||
/**
|
||||
* Encrypted message holder.
|
||||
*
|
||||
* Do not instantiate it directly, use [PublicKey.encryptMessage], [PublicKey.encryptAnonymousMessage], etc.
|
||||
* instead. Also [SecretKey.decrypt] can be used to decrypt it same as [decrypt] or [decryptWithSenderKey].
|
||||
* Do not instantiate it directly, use [EncryptingPublicKey.encryptMessage], [EncryptingPublicKey.encryptAnonymousMessage], etc.
|
||||
* 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.
|
||||
* This class carries all this information; serialize and pass it to the recipient.
|
||||
@@ -41,12 +51,12 @@ object Asymmetric {
|
||||
class Message(
|
||||
private val nonce: 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)
|
||||
}
|
||||
|
||||
@@ -54,7 +64,7 @@ object Asymmetric {
|
||||
* 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.
|
||||
*/
|
||||
fun decryptWithSenderKey(senderKey: PublicKey, recipientKey: SecretKey): UByteArray {
|
||||
fun decryptWithSenderKey(senderKey: EncryptingPublicKey, recipientKey: DecryptingSecretKey): UByteArray {
|
||||
return try {
|
||||
WithFill.decode(
|
||||
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
|
||||
* 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
|
||||
* 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 recipient the recipients' public key.
|
||||
* @param plainData data to encrypt
|
||||
*/
|
||||
internal fun createMessage(
|
||||
from: SecretKey, recipient: PublicKey, plainData: UByteArray,
|
||||
from: DecryptingSecretKey, recipient: EncryptingPublicKey, plainData: UByteArray,
|
||||
nonce: UByteArray = randomNonce(),
|
||||
): Message {
|
||||
return Message(
|
||||
@@ -104,8 +114,8 @@ object Asymmetric {
|
||||
|
||||
private fun randomNonce(): UByteArray = randomUBytes(crypto_box_NONCEBYTES)
|
||||
|
||||
fun generateKeys() = SecretKey.generateKeys()
|
||||
fun newSecretKey() = SecretKey.new()
|
||||
fun generateKeys() = DecryptingSecretKey.generateKeys()
|
||||
fun newSecretKey() = DecryptingSecretKey.new()
|
||||
|
||||
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
|
||||
* 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
|
||||
|
||||
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
|
||||
@@ -87,15 +97,15 @@ open class BinaryId(
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
val asPublicKey: PublicKey by lazy {
|
||||
val asPublicKey: EncryptingPublicKey by lazy {
|
||||
if (magic != KeysmagicNumber.defaultAssymmetric.ordinal)
|
||||
throw InvalidException("It is not a veryfing key: magic=$magic, required ${KeysmagicNumber.defaultAssymmetric.ordinal}")
|
||||
check(body.size == 32)
|
||||
PublicKey(body)
|
||||
EncryptingPublicKey(body)
|
||||
}
|
||||
|
||||
override fun toString(): String = id.encodeToBase64Url()
|
||||
|
||||
@@ -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
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -24,8 +34,8 @@ import net.sergeych.crypto2.Container.Companion.createWith
|
||||
*
|
||||
* Some rules:
|
||||
*
|
||||
* When adding public key recipient, it is faster to use your known [SecretKey], but you
|
||||
* can stay anonymous by just adding [PublicKey] only.
|
||||
* When adding public key recipient, it is faster to use your known [DecryptingSecretKey], but you
|
||||
* can stay anonymous by just adding [EncryptingPublicKey] only.
|
||||
*
|
||||
* 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
|
||||
@@ -107,7 +117,7 @@ sealed class Container {
|
||||
* Add e key to the __decrypted__ container. The new container is also decrypted so you can add
|
||||
* 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
|
||||
@@ -119,7 +129,7 @@ sealed class Container {
|
||||
* Add e key to the __decrypted__ container. The new container is also decrypted so you can add
|
||||
* 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
|
||||
@@ -145,15 +155,9 @@ sealed class Container {
|
||||
abstract val decryptedWithKeyId: KeyId?
|
||||
|
||||
/**
|
||||
* If the container _is decrypted by the [PublicKey]_, e.g., using secret key encryption,
|
||||
* 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.
|
||||
* When the container is decrypted, the key that unlocked it, otherwise null.
|
||||
*/
|
||||
@Transient
|
||||
var authorisedByKey: PublicKey? = null
|
||||
protected set
|
||||
abstract val decryptedWithKey: UniversalKey?
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
|
||||
@Transient
|
||||
private var decryptedWithKey: DecryptingKey? = null
|
||||
override var decryptedWithKey: UniversalKey? = null
|
||||
private set
|
||||
|
||||
override val decryptedWithKeyId: KeyId?
|
||||
get() = decryptedWithKey?.id
|
||||
@@ -192,10 +197,7 @@ sealed class Container {
|
||||
if (k.id == keyId) {
|
||||
kotlin.runCatching { k.decrypt(encryptedMessage) }.getOrNull()?.let {
|
||||
decryptedData = it
|
||||
decryptedWithKey = k
|
||||
if( k is SecretKey) {
|
||||
authorisedByKey = Asymmetric.Message.decode(encryptedMessage).senderPublicKey
|
||||
}
|
||||
decryptedWithKey = k as UniversalKey
|
||||
return it
|
||||
}
|
||||
}
|
||||
@@ -216,7 +218,7 @@ sealed class Container {
|
||||
// otherwise, we don't know the encryption key and will try to derive it
|
||||
// from the decryption key:
|
||||
when (val k = decryptedWithKey!!) {
|
||||
is SecretKey -> {
|
||||
is DecryptingSecretKey -> {
|
||||
key(k.publicKey)
|
||||
}
|
||||
|
||||
@@ -259,12 +261,12 @@ sealed class Container {
|
||||
constructor(key: EncryptingKey, encodeMainKey: UByteArray) :
|
||||
this(key.id, key.encrypt(encodeMainKey))
|
||||
|
||||
constructor(sender: SecretKey?, recipient: PublicKey, encodeMainKey: UByteArray) :
|
||||
constructor(sender: DecryptingSecretKey?, recipient: EncryptingPublicKey, encodeMainKey: UByteArray) :
|
||||
this(
|
||||
recipient.id,
|
||||
recipient.encryptMessage(
|
||||
encodeMainKey,
|
||||
senderKey = sender ?: SecretKey.new(),
|
||||
senderKey = sender ?: DecryptingSecretKey.new(),
|
||||
).encoded
|
||||
)
|
||||
}
|
||||
@@ -274,9 +276,12 @@ sealed class Container {
|
||||
}
|
||||
|
||||
@Transient
|
||||
override var decryptedWithKeyId: KeyId? = null
|
||||
override var decryptedWithKey: UniversalKey? = null
|
||||
private set
|
||||
|
||||
override val decryptedWithKeyId: KeyId?
|
||||
get() = decryptedWithKey?.id
|
||||
|
||||
override val keyIds: List<KeyId> = encryptedKeys.map { it.tag }
|
||||
|
||||
override fun decryptWith(keyRing: UniversalRing): UByteArray? {
|
||||
@@ -292,11 +297,8 @@ sealed class Container {
|
||||
}.getOrNull()?.let { k ->
|
||||
if (kotlin.runCatching { decryptedData = k.decrypt(encryptedMessage) }.isFailure)
|
||||
throw InvalidContainerException()
|
||||
decryptedWithKeyId = key.id
|
||||
decryptedWithKey = key as UniversalKey
|
||||
mainKey = k
|
||||
if( key is SecretKey) {
|
||||
authorisedByKey = Asymmetric.Message.decode(encryptedKey.cipherData).senderPublicKey
|
||||
}
|
||||
}
|
||||
if (decryptedData != null) return decryptedData
|
||||
}
|
||||
@@ -311,6 +313,27 @@ sealed class Container {
|
||||
}.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the container with a password. It scans all key ids for
|
||||
* these with `KDP` params, e.g., derived from password, and try to
|
||||
* derive keys from the password and decrypt the container. If there are
|
||||
* no derivable keys, or all of them failed to decrypt, returns null.
|
||||
* It could be long operation if there are multiple derivable keys with heavy
|
||||
* KDF. See [PBKD] and [KDF] for more.
|
||||
*
|
||||
* @return decrypted data or null
|
||||
*/
|
||||
@Suppress("unused")
|
||||
fun decryptWithPassword(password: String): UByteArray? {
|
||||
for( id in this.keyIds ) {
|
||||
id.kdp?.let { kdp ->
|
||||
decryptWith(kdp.deriveKey(password))?.let { return it }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
@@ -358,8 +381,8 @@ sealed class Container {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one or more [SecretKey] as sender authority coupled with [PublicKey] as
|
||||
* a recipient. This is faster than anonymous usage of [PublicKey] only
|
||||
* Add one or more [DecryptingSecretKey] as sender authority coupled with [EncryptingPublicKey] as
|
||||
* a recipient. This is faster than anonymous usage of [EncryptingPublicKey] only
|
||||
*/
|
||||
fun key(vararg pairs: AsymmetricEncryptionPair) {
|
||||
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.
|
||||
*/
|
||||
fun key(vararg publicKeys: PublicKey) {
|
||||
fun key(vararg publicKeys: EncryptingPublicKey) {
|
||||
keyPairs.addAll(publicKeys.map { null to it })
|
||||
}
|
||||
|
||||
@@ -429,7 +452,7 @@ sealed class Container {
|
||||
Single(
|
||||
pk.id, pk.encryptMessage(
|
||||
plainData,
|
||||
senderKey = sk ?: SecretKey.new(),
|
||||
senderKey = sk ?: DecryptingSecretKey.new(),
|
||||
randomFill = fillRange
|
||||
).encoded,
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
* keys are wise to transfer/save. Concrete implementations are, like [SymmetricKey] or
|
||||
* [SecretKey].
|
||||
* [DecryptingSecretKey].
|
||||
*/
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
import com.ionspin.kotlin.crypto.generichash.GenericHash
|
||||
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import net.sergeych.bintools.ByteChunk
|
||||
import net.sergeych.bintools.asChunk
|
||||
import org.kotlincrypto.hash.sha3.SHA3_256
|
||||
import org.kotlincrypto.hash.sha3.SHA3_384
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* _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
|
||||
* not known beforehand.1
|
||||
* not known beforehand.
|
||||
*
|
||||
* To get a cryptographically safe (more or less) key from password use [KDF] classes, or [KDF.deriveKey]
|
||||
* 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)
|
||||
val result = mutableListOf<UByte>()
|
||||
var round = 0
|
||||
var src = base.encodeToUByteArray()
|
||||
var src = base
|
||||
do {
|
||||
src = "rnd_${round++}_".encodeToUByteArray() + src
|
||||
val hash = digest(src)
|
||||
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)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
* 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.
|
||||
*
|
||||
* - [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 [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]]
|
||||
*
|
||||
* @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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -50,6 +60,12 @@ sealed class Multikey {
|
||||
*/
|
||||
abstract fun check(keys: Iterable<VerifyingPublicKey>): Boolean
|
||||
|
||||
/**
|
||||
* Step towards automated picking necessary keys.
|
||||
* Return all the keys mentioned in this multikey condition.
|
||||
*/
|
||||
abstract fun mentionedKeys(): Set<VerifyingPublicKey>
|
||||
|
||||
/**
|
||||
* Check that [verifyingKeys] satisfy the multikey condition
|
||||
*/
|
||||
@@ -87,6 +103,10 @@ sealed class Multikey {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun mentionedKeys(): Set<VerifyingPublicKey> {
|
||||
return validKeys
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,6 +129,10 @@ sealed class Multikey {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun mentionedKeys(): Set<VerifyingPublicKey> {
|
||||
return validKeys.flatMap { it.mentionedKeys() }.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +143,7 @@ sealed class Multikey {
|
||||
@Serializable
|
||||
object AnyKey : Multikey() {
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -7,20 +17,22 @@ import net.sergeych.bipack.decodeFromBipack
|
||||
import net.sergeych.crypto2.VerifyingPublicKey.Companion.toString
|
||||
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
|
||||
* [SecretKey].
|
||||
* [DecryptingSecretKey].
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("encp")
|
||||
class PublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingKey {
|
||||
class EncryptingPublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingKey {
|
||||
|
||||
override val magic: KeysmagicNumber = KeysmagicNumber.defaultAssymmetric
|
||||
@Transient
|
||||
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
|
||||
* [Asymmetric.Message], so the sender could not be identified.
|
||||
*
|
||||
@@ -57,18 +69,18 @@ class PublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingK
|
||||
fun encryptMessage(
|
||||
plainData: UByteArray,
|
||||
nonce: UByteArray = randomNonce(),
|
||||
senderKey: SecretKey = newSecretKey(),
|
||||
senderKey: DecryptingSecretKey = newSecretKey(),
|
||||
randomFill: IntRange? = null,
|
||||
) = Asymmetric.createMessage(senderKey, this, WithFill.encode(plainData, randomFill), nonce)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
fun encryptMessage(
|
||||
plainData: UByteArray,
|
||||
senderKey: SecretKey,
|
||||
senderKey: DecryptingSecretKey,
|
||||
randomFill: IntRange? = null,
|
||||
): Asymmetric.Message =
|
||||
Asymmetric.createMessage(senderKey, this, WithFill.encode(plainData, randomFill))
|
||||
@@ -81,10 +93,10 @@ class PublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingK
|
||||
* possible)
|
||||
* @throws IllegalArgumentException the public key isn't recognized
|
||||
*/
|
||||
fun parse(text: String): PublicKey {
|
||||
fun parse(text: String): EncryptingPublicKey {
|
||||
val s = text.trim()
|
||||
|
||||
fun parseId(t: String): PublicKey{
|
||||
fun parseId(t: String): EncryptingPublicKey {
|
||||
val id = BinaryId.restoreFromString(t)
|
||||
if (id.magic != KeysmagicNumber.defaultAssymmetric.ordinal)
|
||||
throw IllegalArgumentException("invalid magick ${id.magic} for PublicKey")
|
||||
@@ -100,10 +112,10 @@ class PublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingK
|
||||
// consider it is serialized key in base64 format
|
||||
val data = s.decodeBase64Url().asUByteArray()
|
||||
if (data.size == 32)
|
||||
PublicKey(data)
|
||||
EncryptingPublicKey(data)
|
||||
else {
|
||||
runCatching { data.decodeFromBipack<PublicKey>() }.getOrNull()
|
||||
?: kotlin.runCatching { data.decodeFromBipack<UniversalKey>() as PublicKey }
|
||||
runCatching { data.decodeFromBipack<EncryptingPublicKey>() }.getOrNull()
|
||||
?: kotlin.runCatching { data.decodeFromBipack<UniversalKey>() as EncryptingPublicKey }
|
||||
.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
|
||||
|
||||
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.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import net.sergeych.crypto2.SafeKeyExchange.SessionKey
|
||||
|
||||
/**
|
||||
@@ -10,7 +28,7 @@ import net.sergeych.crypto2.SafeKeyExchange.SessionKey
|
||||
* Usage:
|
||||
*
|
||||
* 1. Create [SafeKeyExchange] on both server and client sides
|
||||
* 2. Exchange [publicKey] instances
|
||||
* 2. Exchange [PublicKey] instances
|
||||
* 3. Create [serverSessionKey] and [clientSessionKey] respectively
|
||||
* 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.
|
||||
* - 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.
|
||||
* - 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 {
|
||||
private val pair = KeyExchange.keypair()
|
||||
@Serializable(with = SafeKeyExchange.SafeKeyExchangeSerializer::class)
|
||||
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
|
||||
* security level and allow using counters as nonce with no extra precautions.
|
||||
*/
|
||||
@Serializable
|
||||
@Serializable(with = SafeKeyExchange.SessionKeySerializer::class)
|
||||
class SessionKey(
|
||||
val sendingKey: EncryptingKey,
|
||||
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.
|
||||
* Do not use it except to get [SessionKey] with [clientSessionKey] or [serverSessionKey]. Storing and reusing
|
||||
* it is a great danger.
|
||||
*
|
||||
* Instances can be compared and used as hashtable keys.
|
||||
*/
|
||||
@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]
|
||||
|
||||
@@ -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
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.sergeych.bipack.BipackEncoder
|
||||
import net.sergeych.bipack.decodeFromBipack
|
||||
import net.sergeych.crypto2.Seal.Companion.create
|
||||
import net.sergeych.utools.now
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Extended public-key signature.
|
||||
@@ -67,7 +78,7 @@ class Seal(
|
||||
*/
|
||||
fun verify(message: UByteArray) {
|
||||
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 {
|
||||
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]
|
||||
* 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
|
||||
* 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
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import net.sergeych.bipack.BipackDecoder
|
||||
@@ -82,6 +92,7 @@ class SealedBox(
|
||||
/**
|
||||
* Unpack bipack-encoded payload
|
||||
*/
|
||||
@Suppress("unused")
|
||||
inline fun <reified T>unpack(): T = BipackDecoder.decode(message)
|
||||
|
||||
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
|
||||
|
||||
import com.ionspin.kotlin.crypto.box.Box
|
||||
import com.ionspin.kotlin.crypto.box.crypto_box_SEEDBYTES
|
||||
import com.ionspin.kotlin.crypto.scalarmult.ScalarMultiplication
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
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
|
||||
* public counterpart, see [publicKey].
|
||||
* public counterpart, see [EncryptingPublicKey].
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("encs")
|
||||
class SecretKey(
|
||||
class DecryptingSecretKey(
|
||||
override val keyBytes: UByteArray,
|
||||
@Transient
|
||||
val _cachedPublicKey: PublicKey? = null,
|
||||
val _cachedPublicKey: EncryptingPublicKey? = null,
|
||||
) : DecryptingKey, UniversalKey() {
|
||||
|
||||
@Transient
|
||||
override val label: String = "sec"
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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.
|
||||
*
|
||||
* @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).
|
||||
* See [decrypt] for more.
|
||||
*/
|
||||
fun decryptWithSenderKey(message: Asymmetric.Message, senderPublicKey: PublicKey): UByteArray =
|
||||
fun decryptWithSenderKey(message: Asymmetric.Message, senderPublicKey: EncryptingPublicKey): UByteArray =
|
||||
message.decryptWithSenderKey(senderPublicKey, this)
|
||||
|
||||
@Transient
|
||||
private var cachedPublicKey: PublicKey? = _cachedPublicKey
|
||||
private var cachedPublicKey: EncryptingPublicKey? = _cachedPublicKey
|
||||
|
||||
/**
|
||||
* The corresponding public key
|
||||
*/
|
||||
val publicKey: PublicKey by lazy {
|
||||
val publicKey: EncryptingPublicKey by lazy {
|
||||
if (cachedPublicKey != null)
|
||||
cachedPublicKey!!
|
||||
else
|
||||
PublicKey(ScalarMultiplication.scalarMultiplicationBase(keyBytes))
|
||||
EncryptingPublicKey(ScalarMultiplication.scalarMultiplicationBase(keyBytes))
|
||||
.also { cachedPublicKey = it }
|
||||
}
|
||||
|
||||
@@ -71,18 +86,46 @@ class SecretKey(
|
||||
get() = 0
|
||||
|
||||
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.
|
||||
*/
|
||||
fun generateKeys(): KeyPair {
|
||||
val p = Box.keypair()
|
||||
val pk = PublicKey(p.publicKey)
|
||||
return KeyPair(SecretKey(p.secretKey, pk), pk)
|
||||
val pk = EncryptingPublicKey(p.publicKey)
|
||||
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
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Instant
|
||||
|
||||
interface SigningKey: KeyInstance {
|
||||
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
|
||||
|
||||
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.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
@@ -66,13 +77,41 @@ class SigningSecretKey(
|
||||
|
||||
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 {
|
||||
val p = Signature.keypair()
|
||||
val publicKey = VerifyingPublicKey(p.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
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
import com.ionspin.kotlin.crypto.secretbox.SecretBox
|
||||
@@ -36,13 +46,54 @@ class SymmetricKey(
|
||||
data class WithNonce(
|
||||
val cipherData: 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 {
|
||||
require(nonce.size == nonceLength)
|
||||
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 id by lazy {
|
||||
@@ -54,6 +105,22 @@ class SymmetricKey(
|
||||
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 {
|
||||
if (this === other) return true
|
||||
if (other !is SymmetricKey) return false
|
||||
|
||||
@@ -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
|
||||
|
||||
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]
|
||||
* 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,
|
||||
* see [KeysmagicNumber].
|
||||
@@ -43,7 +53,7 @@ sealed class UniversalKey : KeyInstance {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newSecretKey() = SecretKey.new()
|
||||
fun newSecretKey() = DecryptingSecretKey.new()
|
||||
fun newSigningKey() = SigningSecretKey.new()
|
||||
|
||||
@Suppress("unused")
|
||||
@@ -57,7 +67,7 @@ sealed class UniversalKey : KeyInstance {
|
||||
val s = text.trim()
|
||||
return when {
|
||||
s.startsWith("\uD83D\uDDDDpub#") || s.startsWith("pub#") ->
|
||||
PublicKey.parse(s)
|
||||
EncryptingPublicKey.parse(s)
|
||||
s.startsWith("\uD83D\uDDDDver#") || s.startsWith("ver#") ->
|
||||
VerifyingPublicKey.parse(s)
|
||||
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
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -14,7 +24,7 @@ import kotlinx.serialization.Transient
|
||||
@SerialName("uprv")
|
||||
class UniversalPrivateKey(
|
||||
val signingKey: SigningSecretKey,
|
||||
val decryptingKey: SecretKey
|
||||
val decryptingKey: DecryptingSecretKey
|
||||
) : UniversalKey(), DecryptingKey by decryptingKey, SigningKey by signingKey {
|
||||
|
||||
override val keyBytes by lazy { signingKey.keyBytes + decryptingKey.keyBytes }
|
||||
@@ -38,10 +48,38 @@ class UniversalPrivateKey(
|
||||
}
|
||||
|
||||
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
|
||||
* 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
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -15,7 +25,7 @@ import kotlinx.serialization.Transient
|
||||
@SerialName("upub")
|
||||
class UniversalPublicKey(
|
||||
val verifyingKey: VerifyingPublicKey,
|
||||
val encryptingKey: PublicKey
|
||||
val encryptingKey: EncryptingPublicKey
|
||||
): UniversalKey(), VerifyingKey by verifyingKey, EncryptingKey by encryptingKey{
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
* [SecretKey], [PublicKey], [VerifyingPublicKey], [SigningSecretKey] and [SymmetricKey],
|
||||
* [DecryptingSecretKey], [EncryptingPublicKey], [VerifyingPublicKey], [SigningSecretKey] and [SymmetricKey],
|
||||
* 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
|
||||
@@ -219,6 +229,8 @@ class UniversalRing(
|
||||
companion object {
|
||||
val EMPTY = UniversalRing(keyWithTags = emptyMap())
|
||||
|
||||
fun new(): UniversalRing = UniversalRing(keyWithTags = emptyMap())
|
||||
|
||||
/**
|
||||
* Join a collection of keyrings together (same as reducing with `+`). Correctly
|
||||
* 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
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.
|
||||
*
|
||||
* 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
|
||||
* [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
|
||||
* 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.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
fun deriveMultiple(password: String, count: Int): List<SymmetricKey> =
|
||||
kdfForSize(count).deriveMultipleKeys(password, count)
|
||||
fun deriveMultiple(password: String, count: Int, salt: UByteArray): List<SymmetricKey> =
|
||||
kdfForSize(count, salt).deriveMultipleKeys(password, count)
|
||||
|
||||
/**
|
||||
* Derive single key from password, same as [deriveMultiple] with count=1.
|
||||
*/
|
||||
fun derive(password: String): SymmetricKey = deriveMultiple(password, 1).first()
|
||||
fun derive(password: String, salt: UByteArray): SymmetricKey = deriveMultiple(password, 1, salt).first()
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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 =
|
||||
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 {
|
||||
if (this === other) return true
|
||||
if (other !is Argon) return false
|
||||
@@ -160,7 +208,7 @@ sealed class KDF {
|
||||
|
||||
fun create(complexity: Complexity, salt: UByteArray, keySize: Int): Argon {
|
||||
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) {
|
||||
FixedLow -> Argon(
|
||||
V2id_13,
|
||||
@@ -209,14 +257,96 @@ sealed class KDF {
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun creteDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF {
|
||||
return Argon.create(complexity, salt, keySize)
|
||||
/**
|
||||
* Fast HKDF derivation for already strong byte material, implemented as described in
|
||||
* [RFC 5869](https://www.rfc-editor.org/rfc/rfc5869).
|
||||
*
|
||||
* HKDF is suitable when [deriveFromBytes] receives source bytes that already contain enough entropy, for
|
||||
* example a shared secret produced by a key agreement protocol, random master key material, or another
|
||||
* high-entropy secret. It is fast by design: it does not slow down brute-force attempts and does not use
|
||||
* extra memory. Do not use it for passwords, memorable phrases, short PINs, user-entered secrets, or other
|
||||
* low-entropy inputs; use [Argon] for those instead.
|
||||
*
|
||||
* The derivation is deterministic for the tuple `(hash, salt, info, keySize, source)`. Store or recreate the
|
||||
* same parameters to derive the same output again. [salt] and [info] are not secret, but changing either one
|
||||
* changes the derived bytes.
|
||||
*
|
||||
* @property hash Hash function to use inside HMAC. The default is [Hash.Sha3_256]. [Hash.Blake2b],
|
||||
* [Hash.Sha3_256], and [Hash.Sha3_384] are supported; [Hash.Sha3AndBlake] is intentionally rejected because
|
||||
* it is a synthetic concatenated hash and has no well-defined HMAC block size.
|
||||
* @property salt Optional extraction salt. Prefer a stable random salt when one is available and can be stored
|
||||
* with the derived data. An empty salt is allowed by HKDF and is treated as a zero-filled salt of the selected
|
||||
* hash output size.
|
||||
* @property info Optional application-specific context string or binary label. Use it to domain-separate
|
||||
* outputs derived from the same source, for example `"container encryption"`, `"header auth"`, or a protocol
|
||||
* transcript hash. It may be empty when there is only one unambiguous use for the derived bytes.
|
||||
* @property keySize Number of bytes to derive. It must be positive and no larger than `255 * hashLen`, where
|
||||
* `hashLen` is the output size of [hash].
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("hkdf")
|
||||
data class HKDF(
|
||||
val hash: Hash = Hash.Sha3_256,
|
||||
val salt: UByteArray = ubyteArrayOf(),
|
||||
val info: UByteArray = ubyteArrayOf(),
|
||||
@Unsigned
|
||||
val keySize: Int,
|
||||
) : KDF() {
|
||||
|
||||
init {
|
||||
require(keySize > 0) { "HKDF key size should be positive" }
|
||||
require(hash.supportsHmac) { "Hash $hash is not supported for HKDF" }
|
||||
require(keySize <= hash.hkdfMaxOutputSize) {
|
||||
"HKDF output for $hash is limited to ${hash.hkdfMaxOutputSize} bytes"
|
||||
}
|
||||
}
|
||||
|
||||
override fun deriveKey(password: String): UByteArray =
|
||||
throw UnsupportedOperationException("HKDF is not a password KDF; use Argon for passwords")
|
||||
|
||||
override fun deriveFromBytes(source: UByteArray): UByteArray =
|
||||
hkdf(hash, source, salt, info, keySize)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is HKDF) return false
|
||||
|
||||
if (hash != other.hash) return false
|
||||
if (keySize != other.keySize) return false
|
||||
if (!salt.contentEquals(other.salt)) return false
|
||||
return info.contentEquals(other.info)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = hash.hashCode()
|
||||
result = 31 * result + salt.contentHashCode()
|
||||
result = 31 * result + info.contentHashCode()
|
||||
result = 31 * result + keySize
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
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")
|
||||
|
||||
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
|
||||
|
||||
@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
|
||||
|
||||
import net.sergeych.synctools.ProtectedOp
|
||||
@@ -5,7 +15,7 @@ import net.sergeych.synctools.invoke
|
||||
|
||||
/**
|
||||
* 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]
|
||||
* property that is thread-safe where there are threads and just safe
|
||||
* 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")
|
||||
|
||||
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
|
||||
|
||||
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")
|
||||
|
||||
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
|
||||
|
||||
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")
|
||||
|
||||
package net.sergeych.utools
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
fun now(): Instant = Clock.System.now()
|
||||
fun nowToSeconds(): Instant = Clock.System.now().truncateToSeconds()
|
||||
|
||||
@@ -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
|
||||
|
||||
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")
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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")
|
||||
package org.komputing.khash.keccak.extensions
|
||||
|
||||
|
||||
@@ -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.initCrypto
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class BinaryIdTest {
|
||||
@@ -13,4 +29,16 @@ class BinaryIdTest {
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -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 kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.encodeToString
|
||||
@@ -24,6 +34,8 @@ class ContainerTest {
|
||||
assertNotNull(d)
|
||||
assertContentEquals(data, d)
|
||||
assertTrue { c1.isDecrypted }
|
||||
assertEquals(syk1.id, c1.decryptedWithKeyId)
|
||||
assertEquals(syk1, c1.decryptedWithKey)
|
||||
|
||||
val data2 = "To push unpushinable".encodeToUByteArray()
|
||||
val c2 = Container.decode(c.updateData(data2).encoded)
|
||||
@@ -51,7 +63,7 @@ class ContainerTest {
|
||||
assertContentEquals(data, d)
|
||||
assertTrue { c1.isDecrypted }
|
||||
assertEquals(p2.publicKey.id, c1.decryptedWithKeyId)
|
||||
assertEquals(p1.publicKey, c1.authorisedByKey)
|
||||
assertEquals(p2.secretKey, c1.decryptedWithKey)
|
||||
|
||||
val data2 = "To push unpushinable".encodeToUByteArray()
|
||||
val c2 = Container.decode(c.updateData(data2).encoded)
|
||||
@@ -78,7 +90,7 @@ class ContainerTest {
|
||||
assertContentEquals(data, d)
|
||||
assertTrue { c1.isDecrypted }
|
||||
assertEquals(p4.publicKey.id, c1.decryptedWithKeyId)
|
||||
assertEquals(p1.publicKey, c1.authorisedByKey)
|
||||
assertEquals(p4.secretKey, c1.decryptedWithKey)
|
||||
|
||||
val data2 = "To push unpushinable".encodeToUByteArray()
|
||||
val c2 = Container.decode(c.updateData(data2).encoded)
|
||||
|
||||
@@ -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.test.runTest
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.time.Clock
|
||||
import net.sergeych.crypto2.Hash
|
||||
import net.sergeych.crypto2.initCrypto
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextUBytes
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
|
||||
@Suppress("UNUSED_PARAMETER", "UNUSED_VARIABLE")
|
||||
suspend fun <T> sw(label: String, f: suspend () -> T): T {
|
||||
val t1 = Clock.System.now()
|
||||
val result = f()
|
||||
val t2 = Clock.System.now()
|
||||
// println("$label: ${t2 - t1}")
|
||||
return result
|
||||
}
|
||||
|
||||
class HashTest {
|
||||
@Test
|
||||
fun testEqualMethods() {
|
||||
@@ -53,17 +55,44 @@ class HashTest {
|
||||
@Test
|
||||
fun deriveSaltTest() = runTest {
|
||||
initCrypto()
|
||||
for( i in 2..257 ) {
|
||||
val x = Hash.Sha3AndBlake.deriveSalt("base one", i)
|
||||
val y = Hash.Sha3AndBlake.deriveSalt("base one", i)
|
||||
val z = Hash.Sha3AndBlake.deriveSalt("base two", i)
|
||||
for (hash in Hash.entries) {
|
||||
for (i in 1..257) {
|
||||
val x = hash.deriveSalt("base one", i)
|
||||
val y = hash.deriveSalt("base one", i)
|
||||
val z = hash.deriveSalt("base two", i)
|
||||
assertContentEquals(x, y)
|
||||
assertFalse { x contentEquals z }
|
||||
assertEquals(x.size, i)
|
||||
assertEquals(y.size, i)
|
||||
assertEquals(z.size, i)
|
||||
if (i > 1) assertFalse { x contentEquals z }
|
||||
assertEquals(i, x.size)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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 net.sergeych.crypto2.Hash
|
||||
import net.sergeych.crypto2.KDF
|
||||
import net.sergeych.crypto2.initCrypto
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class KDFTest {
|
||||
@Test
|
||||
@@ -45,4 +59,159 @@ class KDFTest {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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.encodeToUByteArray
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import net.sergeych.bipack.BipackDecoder
|
||||
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
|
||||
fun secretEncryptTest() = runTest {
|
||||
initCrypto()
|
||||
@@ -108,6 +159,44 @@ class KeysTest {
|
||||
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
|
||||
fun keyExchangeTest() = runTest {
|
||||
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
|
||||
fun asymmetricKeyTest() = runTest {
|
||||
initCrypto()
|
||||
@@ -177,7 +308,7 @@ class KeysTest {
|
||||
// println(sk0.publicKey)
|
||||
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(pk0, sk1.publicKey)
|
||||
// println(j.encodeToString(sk1))
|
||||
@@ -206,9 +337,9 @@ class KeysTest {
|
||||
assertEquals(usy2, usy1)
|
||||
assertFalse { usy1 == usy3 }
|
||||
|
||||
val sk1 = SecretKey.new()
|
||||
val sk2 = SecretKey(sk1.keyBytes)
|
||||
val sk3 = SecretKey.new()
|
||||
val sk1 = DecryptingSecretKey.new()
|
||||
val sk2 = DecryptingSecretKey(sk1.keyBytes)
|
||||
val sk3 = DecryptingSecretKey.new()
|
||||
|
||||
assertEquals(sk1, sk2)
|
||||
assertEquals(sk2, sk1)
|
||||
@@ -408,13 +539,13 @@ class KeysTest {
|
||||
@Test
|
||||
fun testEncodedSizes2() = runTest {
|
||||
initCrypto()
|
||||
val x = SecretKey.new()
|
||||
val x = DecryptingSecretKey.new()
|
||||
// println("key bytes: ${x.keyBytes.size}:\n${x.keyBytes.toDump()}")
|
||||
val y = BipackEncoder.encode(x)
|
||||
// println("packed: ${y.size}: ${y.toDump()}")
|
||||
assertTrue { x.keyBytes.size + 5 > y.size }
|
||||
assertEquals(x, BipackDecoder.decode<SecretKey>(y))
|
||||
assertContentEquals(x.keyBytes, BipackDecoder.decode<SecretKey>(y).keyBytes)
|
||||
assertEquals(x, BipackDecoder.decode<DecryptingSecretKey>(y))
|
||||
assertContentEquals(x.keyBytes, BipackDecoder.decode<DecryptingSecretKey>(y).keyBytes)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -422,7 +553,7 @@ class KeysTest {
|
||||
initCrypto()
|
||||
val k1 = SigningSecretKey.new()
|
||||
val k2 = k1.verifyingKey
|
||||
val k3 = SecretKey.new()
|
||||
val k3 = DecryptingSecretKey.new()
|
||||
val k4 = k3.publicKey
|
||||
|
||||
val k5 = UniversalPrivateKey.new()
|
||||
@@ -432,7 +563,7 @@ class KeysTest {
|
||||
assertContentEquals(k2.keyBytes, k2.id.id.body)
|
||||
|
||||
val k7 = SymmetricKey.new()
|
||||
val k8 = KDF.Complexity.Interactive.derive("super")
|
||||
val k8 = KDF.Complexity.Interactive.derive("super", KDF.Argon.randomSalt())
|
||||
|
||||
fun testToString(k: UniversalKey) {
|
||||
val s = k.toString()
|
||||
|
||||
@@ -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 net.sergeych.crypto2.PBKD
|
||||
import net.sergeych.crypto2.initCrypto
|
||||
|
||||
@@ -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.datetime.Instant
|
||||
import kotlin.time.Instant
|
||||
import net.sergeych.crypto2.initCrypto
|
||||
import net.sergeych.utools.nowToSeconds
|
||||
import net.sergeych.utools.pack
|
||||
|
||||
@@ -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 kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.bintools.toDump
|
||||
@@ -17,7 +27,7 @@ class RingTest {
|
||||
assertEquals(y1, y2)
|
||||
|
||||
val e1 = Asymmetric.newSecretKey()
|
||||
val e2: SecretKey = BipackDecoder.decode(BipackEncoder.encode(e1))
|
||||
val e2: DecryptingSecretKey = BipackDecoder.decode(BipackEncoder.encode(e1))
|
||||
assertEquals(e1, e2)
|
||||
|
||||
val k1 = SymmetricKey("1234567890Hello,dolly.here-we-go".encodeToUByteArray()) as UniversalKey
|
||||
@@ -154,14 +164,14 @@ class RingTest {
|
||||
|
||||
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(b, r1.findKey<SigningKey>(b.id))
|
||||
assertEquals(c, r1.findById(c.id).first())
|
||||
|
||||
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(b, r1.findKey<SigningKey>(b.id))
|
||||
assertEquals(c, r1.findById(c.id).first())
|
||||
|
||||
@@ -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()}")
|
||||
}
|
||||
@@ -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 net.sergeych.bintools.encodeToHex
|
||||
import net.sergeych.crypto2.BinaryId
|
||||
|
||||
@@ -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
|
||||
|
||||
inline fun <reified T: Throwable>assertThrows(f: ()->Unit): T {
|
||||
|
||||
@@ -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.BipackEncoder
|
||||
|
||||
|
||||
Reference in new issue
Block a user