Compare commits

...

36 Commits

Author SHA1 Message Date
b86ac6f00b 0.8.4 for all platforms 2025-03-27 00:17:59 +03:00
9d338d2f13 merging 2025-03-27 00:14:32 +03:00
5228be33ee fixed README.md 2025-03-26 23:45:31 +03:00
528439f61d 0.8.3 for all platforms 2025-03-26 23:38:40 +03:00
7c97a843e7 Merge pull request 'multiplatform-crypto-libsodium-bindings version with all targets' (#11) from YoungBlood/crypto2:master into master
Reviewed-on: #11
2025-03-25 21:05:37 +03:00
kildishevps
1a81cd5110 multiplatform-crypto-libsodium-bindings version with all targets 2025-03-24 00:53:28 +03:00
7d3e396cf7 dependency simplified 2025-03-16 22:29:22 +03:00
85b13ed8ca EncryptedKVStorage: can destroy exisint creating one with a new key 2025-03-16 16:53:05 +03:00
fbbe4d3a34 EncryptedKVStorage: can destroy exisint creating one with a new key 2025-03-16 16:52:26 +03:00
875c0f7a50 fix #10 KDF.Complexity derivation functions now require salt 2025-03-12 23:39:26 +03:00
fe6190eb8d +EncryptedKVStorage 2025-03-12 23:00:40 +03:00
776f4e75ff 0.8.3-SNAPSHOT use updated bintools 2025-03-08 02:11:13 +03:00
fa7263b0e7 0.8.3-SNAPSHOT uaw mp_bintools bytechunk for compatibility 2025-03-08 01:53:35 +03:00
bd81f88dd8 0.8.2-SNAPSHOT support for wasmJs using youngblood, no mingw yet 2025-03-07 15:14:54 +03:00
6fcf7841a7 Merge pull request 'Change multiplatform-crypto-libsodium-bindings version and bring wasmJs back' (#9) from YoungBlood/crypto2:master into master
Reviewed-on: #9
2025-03-07 09:31:04 +03:00
kildishevps
4748ea0d65 Change commonMain multiplatform-crypto-libsodium-bindings version and bring wasmJs back 2025-03-03 12:48:10 +03:00
e2d4fb07ad 0.7.4-SNAPSHOT back to no wasm; public/secret key renamed with compatibility aliases and deprecation warnings 2025-02-24 09:26:11 +03:00
d180da309b added copyright, license mention and other #10x2linus stuff. 2025-02-20 12:21:42 +03:00
c9e3c57ee2 0.8.1 started: experimental support of the wasmJS target. 2025-02-13 10:04:16 +03:00
81e02ac88e Merge pull request 'Fixing multiplatform-crypto-libsodium-bindings dependencies' (#8) from YoungBlood/crypto2:master into master
Reviewed-on: #8
2025-02-13 09:54:58 +03:00
kildishevps
e8d6b2fc02 Fixing multiplatform-crypto-libsodium-bindings dependencies 2025-02-09 21:59:28 +03:00
277dc62553 Merge pull request 'Add wasmJs target' (#7) from YoungBlood/crypto2:master into master
Reviewed-on: #7
2025-02-09 16:57:38 +03:00
7e52a72c6a Add wasmJs target 2025-02-05 16:39:10 +03:00
7fa3ab1ca8 seal now allows up to 45s in future to tolerate some time difference 2025-02-03 17:13:28 +03:00
242cc7d0f5 more tests 2024-11-29 15:42:54 +07:00
db7453fbb2 +id now has public constructor
+default bas64 repr for ByteChunk
2024-11-29 11:25:01 +07:00
cef7e4abed add general support from encoding to string and restoring from string various keys formats. + docs. 2024-11-27 18:11:45 +07:00
e8fa634640 0.7.1 started. fixed secret key serialization excessive size for signing/verifying keys 2024-11-26 18:50:07 +07:00
10ec58ec08 +hash salt derivation for any size. better naming 2024-11-25 17:11:08 +07:00
9f7babdf58 extending to be more convenient 2024-11-21 15:12:24 +07:00
640ceb448e fix #1 UniversalPrivateKey & UniversalPublicKey 2024-11-13 18:24:44 +07:00
194fe22afa hash StreamProcessor is now public
better docs on BinaryId
some sugar
2024-09-28 00:35:07 +03:00
8eed7a3de7 v0.5.8: Multikeys 2024-09-22 23:04:55 +03:00
4cbc17334c +Multikey.AnyKey 2024-09-22 22:54:31 +03:00
1191de284e 0.5.8-SNAPSHOT: Multikeys 2024-09-15 12:29:04 +03:00
8e652e0421 0.5.8-SNAPSHOT: Multikeys 2024-09-12 18:10:41 +03:00
77 changed files with 1887 additions and 197 deletions

2
.gitignore vendored
View File

@ -9,6 +9,7 @@ build/
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
.idea
*.iws
*.iml
*.ipr
@ -41,4 +42,5 @@ out/
# Other
.kotlin
.idea
.gigaide
/kotlin-js-store/yarn.lock

7
.idea/.gitignore generated vendored
View File

@ -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

View File

@ -1,6 +0,0 @@
<component name="ArtifactManager">
<artifact type="jar" name="crypto2-js-0.1.1-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="crypto2-js-0.1.1-SNAPSHOT.jar" />
</artifact>
</component>

View File

@ -1,8 +0,0 @@
<component name="ArtifactManager">
<artifact type="jar" name="crypto2-js-1.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="crypto2-js-1.0-SNAPSHOT.jar">
<element id="module-output" name="crypto2.jsMain" />
</root>
</artifact>
</component>

View File

@ -1,6 +0,0 @@
<component name="ArtifactManager">
<artifact type="jar" name="crypto2-jvm-0.1.1-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="crypto2-jvm-0.1.1-SNAPSHOT.jar" />
</artifact>
</component>

View File

@ -1,8 +0,0 @@
<component name="ArtifactManager">
<artifact type="jar" name="crypto2-jvm-1.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="crypto2-jvm-1.0-SNAPSHOT.jar">
<element id="module-output" name="crypto2.jvmMain" />
</root>
</artifact>
</component>

View File

@ -1,8 +0,0 @@
<component name="ArtifactManager">
<artifact type="jar" name="crypto2-wasm-js-0.1.1-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="crypto2-wasm-js-0.1.1-SNAPSHOT.jar">
<element id="module-output" name="crypto2.wasmJsMain" />
</root>
</artifact>
</component>

View File

@ -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>

2
.idea/gradle.xml generated
View File

@ -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>

View File

@ -1,7 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ReplaceUntilWithRangeUntil" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="StructuralWrap" enabled="false" level="TYPO" enabled_by_default="false" />
</profile>
</component>

6
.idea/kotlinc.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.20" />
</component>
</project>

7
.idea/misc.xml generated
View File

@ -1,9 +1,4 @@
<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">
<output url="file://$PROJECT_DIR$/out" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17 (2)" project-jdk-type="JavaSDK" />
</project>

View File

@ -2,12 +2,16 @@
Kotlin Multiplatform cryptographic primitives using modern strong cryptography.
## 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.
# Important notes on upgrade
___Please upgrade to 0.7.1+___ as it has much more compact but not backward-compatible serialization format!
Since version __0.5.*__ key identity calculation for asymmetric keys is updated
to make it safer for theoretic future attack on blake2b hashing. Key.id values
are incompatible with older. Sorry for inconvenience.
@ -19,7 +23,7 @@ repositories {
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
}
dependencies {
import("net.sergeych:crypto2:0.5.7")
import("net.sergeych:crypto2:0.8.4")
}
```
@ -34,6 +38,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
@ -108,4 +116,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

View File

@ -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

View File

@ -1,24 +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.0.21"
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.21"
id("org.jetbrains.dokka") version "1.9.20"
`maven-publish`
}
group = "net.sergeych"
version = "0.5.7"
version = "0.8.4"
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
}
@ -36,9 +50,10 @@ 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 {
@ -47,18 +62,16 @@ kotlin {
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
}
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("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("net.sergeych:mp_bintools:0.1.12")
}
}
val commonTest by getting {
@ -78,7 +91,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) }
}
}

View File

@ -1 +1,11 @@
#
# 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

View File

@ -1,3 +1,13 @@
#
# Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
#
# You may use, distribute and modify this code under the
# terms of the private license, which you must obtain from the author
#
# To obtain the license, contact the author: https://t.me/real_sergeych or email to
# real dot sergeych at gmail.
#
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip

16
gradlew vendored
View File

@ -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.
#
##############################################################################

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
pluginManagement {
repositories {
mavenCentral()

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
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>

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
@ -5,17 +15,60 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import net.sergeych.bintools.CRC
import net.sergeych.bintools.CRC8
import net.sergeych.crypto2.BinaryId.Companion.createFromBytes
import net.sergeych.crypto2.BinaryId.Companion.createFromString
import net.sergeych.crypto2.BinaryId.Companion.createFromUBytes
import net.sergeych.crypto2.BinaryId.Companion.createRandom
import net.sergeych.crypto2.BinaryId.IncomparableException
import net.sergeych.crypto2.BinaryId.InvalidException
import net.sergeych.mp_tools.decodeBase64Url
import kotlin.random.Random
/**
* Binary identifier with control code and magic number. To create instaance
* use one of [createFromBytes], [createFromString], [createFromUBytes],
* or [createRandom], also deserialize serialized one.
*
* Integrity is checked on instantiating automatically.
*
* It is comparable to other BinaryId as long as both have the same [magic]. Attempt to
* compare these that differ throws [IncomparableException]
*
* ### Internal structure
*
* Say we have a `BinaryId` of size `N` bytes. The inner structure will be:
*
* | offset | meaning |
* |-----------|---------|
* | 0 ..< N-2 | id bytes |
* | N-2 | magic, 0..255 |
* | N-1 | CRC8, polynomial 0xA7, as in Bluetooth |
*
* @throws InvalidException if crc check failed
*/
@Serializable
open class BinaryId protected constructor (
open class BinaryId(
/**
* The packed binary id. Note that serialized version is one byte longer containing
* the size prefix
*/
val id: UByteArray,
) : Comparable<BinaryId> {
class InvalidException(text: String) : IllegalArgumentException(text)
/**
* Bad format (crc does not match)
*/
class InvalidException(text: String, reason: Throwable? = null) : IllegalArgumentException(text, reason)
/**
* Attempt to compare binary ids with different magic. In this case only [equals]
* works, but [compareTo] throws this exception.
*/
class IncomparableException(text: String) : IllegalArgumentException(text)
/**
* magic number (as decoded), `0..255`
*/
@Transient
val magic: Int = run {
if (id.size < 4) throw InvalidException("BinaryId is too short")
@ -27,29 +80,41 @@ open class BinaryId protected constructor (
rest.last().toInt()
}
private val innerData: UByteArray by lazy { id.sliceArray( 1..< id.size-1 ) }
private val innerData: UByteArray by lazy { id.sliceArray(1..<id.size - 1) }
/**
* The id body: all the bytes except check and magic. These could carry useful information.
* The ID body: all the bytes except check and magic. ID bytes could carry useful information.
*
* - `id.size` is [body] size + 2 (see [BinaryId] inner structure)
*/
val body: UByteArray by lazy { id.sliceArray( 0 until id.size-2 ) }
val body: UByteArray by lazy { id.sliceArray(0 until id.size - 2) }
val asVerifyingKey: VerifyingKey by lazy {
if( magic != KeysmagicNumber.defaultVerifying.ordinal)
if (magic != KeysmagicNumber.defaultVerifying.ordinal)
throw InvalidException("It is not a veryfing key: magic=$magic, required ${KeysmagicNumber.defaultVerifying.ordinal}")
check(body.size == 32)
VerifyingPublicKey(body)
}
val asPublicKey: PublicKey by lazy {
if( magic != KeysmagicNumber.defaultAssymmetric.ordinal)
/**
* 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: 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()
/**
* Compare to another ID which __must have the same [magic]__ number; note that [equals]
* works well despite magic inequity.
* @throws IncomparableException if magic id do not match
*/
override fun compareTo(other: BinaryId): Int {
if (other.magic != magic) throw IncomparableException("Mask mismatch (my=$magic their=${other.magic})")
val id1 = other.id
@ -81,8 +146,9 @@ open class BinaryId protected constructor (
* Restore a string representation of existing BinaryId.
*/
@Suppress("unused")
fun restoreFromString(str: String): BinaryId =
fun restoreFromString(str: String): BinaryId = kotlin.runCatching {
BinaryId(str.decodeBase64Url().toUByteArray())
}.getOrElse { throw InvalidException("can't parse binary id: $str", it) }
fun createFromBytes(magic: Int, bytes: ByteArray): BinaryId = createFromUBytes(magic, bytes.toUByteArray())
@ -101,8 +167,8 @@ open class BinaryId protected constructor (
}
@Suppress("unused")
fun createRandom(magicNumber: Int, size: Int=16) =
createFromBytes(magicNumber, Random.Default.nextBytes(size-2))
fun createRandom(magicNumber: Int, size: Int = 16) =
createFromBytes(magicNumber, Random.Default.nextBytes(size - 2))
/**
* Encode a string as UTF and create a binaryId from its bytes and provided magic.

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import kotlinx.serialization.SerialName
@ -20,10 +30,12 @@ import net.sergeych.crypto2.Container.Companion.createWith
* - [addRecipients] and various [plus] operators to add recipients
* - [updateData] to change decrypted content for the same recipient keys
*
* Note that container _is serialized encrypted_.
*
* 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
@ -105,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
@ -117,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
@ -126,8 +138,9 @@ sealed class Container {
abstract fun updateData(newPlainData: UByteArray, randomFill: IntRange? = null): Container
/**
* Binary encoded version. It is desirable to include [Container] as an object, though,
* especially when using custom serialization (Json, Boss, etc), it is serializable.
* Binary encoded _encrypted_ version. It is desirable to include [Container] as an object, though,
* especially when using custom serialization (Json, Boss, etc.), it is serializable. Note that
* serialized data is always encrypted.
* Still, if you need it in binary form, this is a shortcut. You can use [decode] or call
* [BipackDecoder.decode] to deserialize the binary form.
*/
@ -142,16 +155,21 @@ 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
* If the container _is decrypted by the [EncryptingPublicKey]_, e.g., using secret key encryption,
* contains the [EncryptingPublicKey] that corresponds the [DecryptingSecretKey] used while encrypting, this
* authenticating the sender party cryptographically. This key could be used to encrypt
* the response to be visible to the sender only; the sender, providing it kept his secret key,
* could decrypt it.
*/
@Transient
var authorisedByKey: PublicKey? = null
var authorisedByKey: EncryptingPublicKey? = null
protected set
/**
* List of [KeyId] of the keys that unlocked the container, in the same order used for encryption..
*/
abstract val keyIds: List<KeyId>
/**
* @suppress
@ -172,6 +190,8 @@ sealed class Container {
override val decryptedWithKeyId: KeyId?
get() = decryptedWithKey?.id
override val keyIds: List<KeyId> = listOf(keyId)
init {
decryptedData = creationData
}
@ -183,7 +203,7 @@ sealed class Container {
kotlin.runCatching { k.decrypt(encryptedMessage) }.getOrNull()?.let {
decryptedData = it
decryptedWithKey = k
if( k is SecretKey) {
if(k is DecryptingSecretKey) {
authorisedByKey = Asymmetric.Message.decode(encryptedMessage).senderPublicKey
}
return it
@ -206,7 +226,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)
}
@ -249,12 +269,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
)
}
@ -267,6 +287,8 @@ sealed class Container {
override var decryptedWithKeyId: KeyId? = null
private set
override val keyIds: List<KeyId> = encryptedKeys.map { it.tag }
override fun decryptWith(keyRing: UniversalRing): UByteArray? {
decryptedData?.let { return it }
for (key in keyRing.decryptingKeys) {
@ -282,7 +304,7 @@ sealed class Container {
throw InvalidContainerException()
decryptedWithKeyId = key.id
mainKey = k
if( key is SecretKey) {
if(key is DecryptingSecretKey) {
authorisedByKey = Asymmetric.Message.decode(encryptedKey.cipherData).senderPublicKey
}
}
@ -346,8 +368,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)
@ -356,7 +378,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 })
}
@ -417,7 +439,7 @@ sealed class Container {
Single(
pk.id, pk.encryptMessage(
plainData,
senderKey = sk ?: SecretKey.new(),
senderKey = sk ?: DecryptingSecretKey.new(),
randomFill = fillRange
).encoded,
plainData,
@ -474,6 +496,12 @@ sealed class Container {
inline fun <reified T> decrypt(cipherData: UByteArray, vararg keys: DecryptingKey): T? =
decryptAsUBytes(cipherData, *keys)?.let { BipackDecoder.decode<T>(it.asByteArray()) }
inline fun <reified T> decrypt(cipherData: UByteArray, ring: UniversalRing): T? =
decode(cipherData)
.decryptWith(ring)?.let {
BipackDecoder.decode<T>(it.asByteArray())
}
fun decryptAsUBytes(cipherData: UByteArray, vararg keys: DecryptingKey): UByteArray? =
decode(cipherData).decryptWith(*keys)

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import net.sergeych.bintools.CRC

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
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 {
/**

View File

@ -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)
}
}
}

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.util.encodeToUByteArray

View File

@ -1,13 +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.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
private interface StreamProcessor {
interface StreamProcessor {
fun update(data: UByteArray)
fun final(): UByteArray
}
@ -23,7 +35,7 @@ private interface StreamProcessor {
@Suppress("unused")
enum class Hash(
private val direct: ((UByteArray) -> UByteArray)? = null,
private val streamProcessor: () -> StreamProcessor,
val streamProcessor: () -> StreamProcessor,
) {
Blake2b(
@ -112,6 +124,33 @@ enum class Hash(
return sp.final()
}
/**
* Derive a salt of any size from a text. ___Salt could not be used as a password key___ source as it is not
* 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
* 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
*
* 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 {
require(sizeInBytes > 0)
val result = mutableListOf<UByte>()
var round = 0
var src = base.encodeToUByteArray()
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))
} while (result.size < sizeInBytes)
return result.toUByteArray()
}
}
private val defaultSuffix1 = "All lay loads on a willing horse".encodeToUByteArray()
@ -122,6 +161,8 @@ private val defaultSuffix2 = "A stitch in time saves nine".encodeToUByteArray()
*/
fun blake2b(src: UByteArray): UByteArray = Hash.Blake2b.digest(src)
fun blake2b(src: ByteChunk): ByteChunk = blake2b(src.data).asChunk()
/**
* Double linked Blake2b using the default or specified suffix. This should be more hard to
* brute force.collision attack than just [blake2b]. Note that different suffixes provide different

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.LibsodiumInitializer

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
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,6 +28,9 @@ import kotlinx.serialization.Serializable
*
* See [PBKD.Params.deriveKey] for deriving keys from id.
*
* 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.
* @param kdp optional key derivation parameters. Does not affect equality. Allow deriving the key from proper
* password, see above.
@ -28,6 +41,8 @@ data class KeyId(val id: BinaryId, val kdp: PBKD.Params? = null) {
/**
* Binary array representation of the [id], not including the [kdp]. Used in [SafeKeyExchange]
* and other key exchanges to generate session tokens, etc.
*
* In shortcut for packed [BinaryId], from [id]. If you need only key bytes, use [UniversalKey.keyBytes].
*/
val binaryTag: UByteArray by lazy { id.id }

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
/**

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
enum class KeysmagicNumber(val label: String) {
@ -6,6 +16,11 @@ enum class KeysmagicNumber(val label: String) {
defaultSymmetric( "sym"),
defaultSession( "ssn"),
defaultVerifying( "ver"),
defaultSigningSecret( "sig"),
defaultUniversalPublic( "pub+"),
defaultUniversalPrivate( "prv+"),
;
}

View File

@ -0,0 +1,207 @@
/*
* 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
import kotlinx.serialization.Serializable
import net.sergeych.bipack.Unsigned
import net.sergeych.crypto2.Multikey.AnyKey
import net.sergeych.crypto2.Multikey.Companion.allOf
import net.sergeych.crypto2.Multikey.Companion.allOfMultikeys
import net.sergeych.crypto2.Multikey.Companion.anyOf
import net.sergeych.crypto2.Multikey.Companion.anyOfMultikeys
import net.sergeych.crypto2.Multikey.Companion.invoke
import net.sergeych.crypto2.Multikey.Companion.someOf
import net.sergeych.crypto2.Multikey.Companion.someOfMultikeys
/**
* Multi-signed key.
* An arbitrary combination of [VerifyingPublicKey] to implement any multiple keys scenario, like N of M,
* and logical expression. Sample usage:
*
* ```kotlin
* val k1 = SigningSecretKey.new().verifyingKey
* val k2 = SigningSecretKey.new().verifyingKey
* val k3 = SigningSecretKey.new().verifyingKey
* val k4 = SigningSecretKey.new().verifyingKey
*
* val multikey = (k1 or k2) and (k3 or k4)
*
* val b: SealedBox = SealedBox.decode(someData)
*
* if( b.isSealedBy(multikey) ) {
* println("sealed box is properly sealed by a multikey")
* }
* ```
* To build multikeys, use `and` and `or` infix operators against [VerifyingPublicKey], [Multikey], or even
* [SigningSecretKey] instances, and shortcut methods:
*
* - [someOfMultikeys], [someOf] family for `n of M` logic
* - [anyOfMultikeys], [anyOf], [allOf], and [allOfMultikeys]
* - [invoke] for a single-key multikey
* - [AnyKey] when you need effectively match any key, useful when you need a `var` `Multikey`.
*
* __Important__. When serializing, always serialize as root [Multikey] instance to keep
* it compatible with any combination.
*/
@Serializable
sealed class Multikey {
/**
* Check that the [keys] satisfy the condition of this instance
*/
abstract fun check(keys: Iterable<VerifyingPublicKey>): Boolean
/**
* Check that [verifyingKeys] satisfy the multikey condition
*/
fun check(vararg verifyingKeys: VerifyingPublicKey): Boolean = check(verifyingKeys.asIterable())
infix fun or(mk: Multikey): Multikey = SomeOf(1, listOf(this, mk))
infix fun or(k: VerifyingPublicKey) = SomeOf(1, listOf(this, Multikey(k)))
infix fun or(k: SigningSecretKey) = SomeOf(1, listOf(this, Multikey(k.verifyingKey)))
infix fun and(mk: Multikey): Multikey = SomeOf(2, listOf(this, mk))
infix fun and(k: VerifyingPublicKey) = SomeOf(2, listOf(this, Multikey(k)))
infix fun and(k: SigningSecretKey) = SomeOf(2, listOf(this, Multikey(k.verifyingKey)))
/**
* Multikey instance implementing `m of N` logic against [VerifyingPublicKey] set. Do not use
* it directly, use any [Multikey.someOfMultikeys] functions instead.
*/
@Serializable
@SerialName("k")
class Keys internal constructor(
@Unsigned
val requiredMinimum: Int,
val validKeys: Set<VerifyingPublicKey>,
) : Multikey() {
override fun check(keys: Iterable<VerifyingPublicKey>): Boolean {
var matches = 0
for (signer in keys) {
if (signer in validKeys) {
if (++matches >= requiredMinimum) return true
}
}
return false
}
}
/**
* Multikey instance implementing `m of N` logic against other [Multikey] instances. Do not use
* it directly, use any [Multikey.someOfMultikeys] functions instead.
*/
@Serializable
@SerialName("n")
class SomeOf internal constructor(
@Unsigned
val requiredMinimum: Int,
val validKeys: List<Multikey>,
) : Multikey() {
override fun check(keys: Iterable<VerifyingPublicKey>): Boolean {
var matches = 0
for (k in validKeys) {
if (k.check(keys)) {
if (++matches >= requiredMinimum) return true
}
}
return false
}
}
/**
* Special `AnyKey`: no restrictions, any key will satisfy this. In the rare case to mark
* publicly available operations, etc. Please note it is an object, not a class, and can't
* be instantiated.
*/
@Serializable
object AnyKey : Multikey() {
override fun check(keys: Iterable<VerifyingPublicKey>): Boolean = true
}
companion object {
operator fun invoke(k: SigningSecretKey): Multikey = Keys(1, setOf(k.verifyingKey))
operator fun invoke(k: VerifyingPublicKey): Multikey = Keys(1, setOf(k))
/**
* Create a multikey instance that requires some keys from a list
*/
fun someOf(requiredMinimum: Int, vararg keys: VerifyingPublicKey): Multikey =
Keys(requiredMinimum, keys.toSet())
/**
* Create a multikey instance that requires some keys from a list
*/
fun someOfMultikeys(requiredMinimum: Int, vararg keys: Multikey): Multikey =
SomeOf(requiredMinimum, keys.toList())
/**
* Create a multikey instance that requires some keys from a list
*/
fun someOfMultikeys(requiredMinimum: Int, keys: List<Multikey>): Multikey =
SomeOf(requiredMinimum, keys)
/**
* Create a multikey instance that requires some keys from a list
*/
fun someOf(requiredMinimum: Int, keys: List<VerifyingPublicKey>): Multikey =
Keys(requiredMinimum, keys.toSet())
/**
* Create a multikey instance that requires any key from a list
*/
fun anyOf(vararg keys: VerifyingPublicKey): Multikey = someOf(1, *keys)
/**
* Create a multikey instance that requires any key from a list
*/
fun anyOfMultikeys(vararg keys: Multikey): Multikey = someOfMultikeys(1, *keys)
/**
* Create a multikey instance that requires any key from a list
*/
fun anyOfMultikeys(keys: List<Multikey>): Multikey = someOfMultikeys(1, keys)
/**
* Create a multikey instance that requires any key from a list
*/
fun anyOf(keys: List<VerifyingPublicKey>): Multikey = someOf(1, keys)
/**
* Create a multikey instance that requires all keys from a list
*/
fun allOf(vararg keys: VerifyingPublicKey): Multikey = someOf(keys.size, *keys)
/**
* Create a multikey instance that requires all keys from a list
*/
fun allOfMultikeys(vararg keys: Multikey): Multikey = someOfMultikeys(keys.size, *keys)
/**
* Create a multikey instance that requires all keys from a list
*/
fun allOfMultikeys(keys: List<Multikey>): Multikey = someOfMultikeys(keys.size, keys)
/**
* Create a multikey instance that requires all keys from a list
*/
fun allOf(keys: List<VerifyingPublicKey>): Multikey = someOf(keys.size, keys)
}
}

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
interface NonceBased {

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
/**

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
@ -114,7 +124,7 @@ object PBKD {
}
return entry.op {
if (entry.data == null) {
entry.data = entry.kdf.derive(password)
entry.data = entry.kdf.deriveKey(password)
}
entry.data!!
}

View File

@ -1,23 +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.
*/
package net.sergeych.crypto2
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
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.
*
@ -54,21 +69,57 @@ 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))
override val id by lazy { KeyId(magic, keyBytes, null, true) }
companion object {
/**
* Parse any known public key text representation, including what [toString] return (for public keys it is
* possible)
* @throws IllegalArgumentException the public key isn't recognized
*/
fun parse(text: String): EncryptingPublicKey {
val s = text.trim()
fun parseId(t: String): EncryptingPublicKey {
val id = BinaryId.restoreFromString(t)
if (id.magic != KeysmagicNumber.defaultAssymmetric.ordinal)
throw IllegalArgumentException("invalid magick ${id.magic} for PublicKey")
return id.asPublicKey
}
// 🗝sig#I1po9Y2I7p2aOxeh4nFyGPm3e0YunBEu1Mo-PmIqP84Evg
return when {
s.startsWith("\uD83D\uDDDDpub#") -> parseId(s.drop(6))
s.startsWith("pub#") -> parseId(s.drop(4))
s.startsWith("#") -> parseId(s.drop(1))
else -> {
// consider it is serialized key in base64 format
val data = s.decodeBase64Url().asUByteArray()
if (data.size == 32)
EncryptingPublicKey(data)
else {
runCatching { data.decodeFromBipack<EncryptingPublicKey>() }.getOrNull()
?: kotlin.runCatching { data.decodeFromBipack<UniversalKey>() as EncryptingPublicKey }
.getOrElse { throw IllegalArgumentException("can't parse verifying key") }
}
}
}
}
}
}

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
@ -10,7 +20,7 @@ import net.sergeych.crypto2.SafeKeyExchange.SessionKey
* Usage:
*
* 1. Create [SafeKeyExchange] on both server and client sides
* 2. Exchange [publicKey] instances
* 2. Exchange [EncryptingPublicKey] instances
* 3. Create [serverSessionKey] and [clientSessionKey] respectively
* 4. Use [SessionKey.sendingKey] and [SessionKey.receivingKey] to send and receive encrypted data.
*
@ -20,7 +30,7 @@ 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.
*/
class SafeKeyExchange {
private val pair = KeyExchange.keypair()

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import kotlinx.datetime.Instant
@ -6,6 +16,7 @@ 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].
*

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import kotlinx.datetime.Instant
@ -6,6 +16,7 @@ import kotlinx.serialization.Transient
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder
import net.sergeych.bipack.decodeFromBipack
import net.sergeych.utools.pack
/**
* Multi-signed data box. Do not use the constructori directly, use [SealedBox.create]
@ -27,11 +38,22 @@ import net.sergeych.bipack.decodeFromBipack
@Serializable
class SealedBox(
val message: UByteArray,
private val seals: List<Seal>,
/**
* [Seal] instance representing _correct signatures_ of this box. Note that if the box
* is constructed (deserialized, etc) successfully, all seals are ok. Initial check
* of signatures could be bypassed by setting [checkOnInit] to false, which should
* be avoided.
*/
val seals: List<Seal>,
@Transient
private val checkOnInit: Boolean = true
) {
/**
* Extract [VerifyingPublicKey] from [seals].
*/
val signedByKeys: List<VerifyingPublicKey> by lazy { seals.map { it.publicKey } }
@Suppress("unused")
constructor(message: UByteArray, vararg keys: SigningKey) :
this(message, keys.map { it.seal(message) } )
@ -49,7 +71,7 @@ class SealedBox(
* Add expiring seal, otherwise use [plus]. Overrides exising seal for [key]
* if present:
*/
fun addSeal(key: SigningKey, expiresAt: Instant): SealedBox {
fun addSeal(key: SigningKey, expiresAt: Instant?): SealedBox {
val filtered = seals.filter { it.publicKey != key.verifyingKey }
return SealedBox(message, filtered + key.seal(message, expiresAt), false)
}
@ -61,6 +83,18 @@ class SealedBox(
return seals.any { it.publicKey == publicKey }
}
/**
* Checks that the box is signed by enough keys to satisfy the given [Multikey].
*/
@Suppress("unused")
fun isSealedBy(multikey: Multikey) = multikey.check(signedByKeys)
/**
* Unpack bipack-encoded payload
*/
@Suppress("unused")
inline fun <reified T>unpack(): T = BipackDecoder.decode(message)
init {
if (seals.isEmpty()) throw IllegalArgumentException("there should be at least one seal")
if (checkOnInit) {
@ -85,6 +119,19 @@ class SealedBox(
return SealedBox(data, keys.map { it.seal(data) }, false)
}
/**
* Create a new instance serializing given data with Bipack and some
* keys. At least one key is required to disallow providing not-signed
* instances, e.g. [SealedBox] is guaranteed to be properly sealed when
* successfully instantiated.
*
* @param payload an object to serialize and sign
* @param keys a list of keys to sign with, should be at least one key.
* @throws IllegalArgumentException if keys are not specified.
*/
inline fun <reified T>new(payload: T,vararg keys: SigningKey): SealedBox =
create(pack(payload), *keys)
inline fun <reified T>encode(value: T, vararg keys: SigningKey): UByteArray =
create(BipackEncoder.encode(value).toUByteArray(), *keys).encoded

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.box.Box
@ -6,24 +16,28 @@ 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 +49,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 +85,18 @@ class SecretKey(
get() = 0
companion object {
data class KeyPair(val secretKey: SecretKey, val publicKey: PublicKey)
data class KeyPair(val secretKey: DecryptingSecretKey, val publicKey: EncryptingPublicKey)
/**
* 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
fun new(): DecryptingSecretKey = generateKeys().secretKey
}
}

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import kotlinx.datetime.Instant

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.signature.Signature
@ -23,16 +33,45 @@ class SigningSecretKey(
VerifyingPublicKey(Signature.ed25519SkToPk(keyBytes)).also { cachedPublicKey = it }
}
override val magic: KeysmagicNumber = KeysmagicNumber.defaultSigningSecret
override fun sign(message: UByteArray): UByteArray = Signature.detached(message, keyBytes)
override fun seal(message: UByteArray, expiresAt: Instant?): Seal =
Seal.create(this, message, now(), expiresAt)
@Transient
override val id: KeyId = verifyingKey.id
override val label: String
get() = "sig"
/**
* Create a [Multikey] that requires presence of this or [other] key
*/
infix fun or(other: VerifyingPublicKey) = Multikey(this) or other
/**
* Create a [Multikey] that requires presence of this or [other] key
*/
infix fun or(other: SigningSecretKey) = Multikey(this) or other
/**
* Create a [Multikey] that requires presence of this or [other] key
*/
infix fun or(other: Multikey) = Multikey(this) or other
/**
* Create a [Multikey] that requires presence of this and [other] key
*/
infix fun and(other: VerifyingPublicKey) = Multikey(this) and other
/**
* Create a [Multikey] that requires presence of this and [other] key
*/
infix fun and(other: SigningSecretKey) = Multikey(this) and other
/**
* Create a [Multikey] that requires presence of this and [other] key
*/
infix fun and(other: Multikey) = Multikey(this) and other
companion object {
data class SigningKeyPair(val secretKey: SigningSecretKey, val publicKey: VerifyingPublicKey)

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.secretbox.SecretBox
@ -46,7 +56,7 @@ class SymmetricKey(
override val nonceBytesLength: Int = nonceLength
override val id by lazy {
KeyId(KeysmagicNumber.defaultSymmetric,blake2b3l(keyBytes), pbkdfParams)
KeyId(KeysmagicNumber.defaultSymmetric,blake2b3l(keyBytes),pbkdfParams)
}
override fun decryptWithNonce(cipherData: UByteArray, nonce: UByteArray): UByteArray =

View File

@ -1,18 +1,39 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import net.sergeych.bipack.BipackEncoder
import net.sergeych.bipack.decodeFromBipack
import net.sergeych.mp_tools.decodeBase64Compact
import net.sergeych.mp_tools.encodeToBase64Compact
@Serializable
sealed class UniversalKey: KeyInstance {
sealed class UniversalKey : KeyInstance {
abstract val keyBytes: UByteArray
@Transient
open val magic: KeysmagicNumber = KeysmagicNumber.Unknown
/**
* 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 [EncryptingPublicKey] and [DecryptingSecretKey].
*
* KeyId is based on [BinaryId] which includes checksum (crc8) and magick number for additional security,
* see [KeysmagicNumber].
*
* Also "public" keys can be restored from id using [BinaryId.asPublicKey] and [BinaryId.asVerifyingKey].
*/
override val id by lazy { KeyId(magic, keyBytes, null) }
// Important: id can be overridden, so we use it, not magic:
@ -32,14 +53,36 @@ sealed class UniversalKey: KeyInstance {
}
companion object {
fun newSecretKey() = SecretKey.new()
fun newSecretKey() = DecryptingSecretKey.new()
fun newSigningKey() = SigningSecretKey.new()
@Suppress("unused")
fun newSymmetricKey() = SymmetricKey.new()
/**
* Parse all known string representations of the universal key
* @throws IllegalArgumentException if it can't parse any key.
*/
fun parseString(text: String): UniversalKey {
val s = text.trim()
return when {
s.startsWith("\uD83D\uDDDDpub#") || s.startsWith("pub#") ->
EncryptingPublicKey.parse(s)
s.startsWith("\uD83D\uDDDDver#") || s.startsWith("ver#") ->
VerifyingPublicKey.parse(s)
else -> {
s.decodeBase64Compact().decodeFromBipack<UniversalKey>()
}
}
}
}
}
inline fun <reified T : UniversalKey> T.asString() =
BipackEncoder.encode<T>(this).encodeToBase64Compact()
open class IllegalSignatureException(text: String = "signed data is tampered or signature is corrupted") :
IllegalStateException(text)

View File

@ -0,0 +1,57 @@
/*
* 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
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
/**
* Combination of private/secret keys suitable for both decryption and signing.
*
* It contains two cryptographically independent keys to raise security to a maximum.
* Any converted keys poses a threat while technically possible so we avoid it.
*/
@Serializable
@SerialName("uprv")
class UniversalPrivateKey(
val signingKey: SigningSecretKey,
val decryptingKey: DecryptingSecretKey
) : UniversalKey(), DecryptingKey by decryptingKey, SigningKey by signingKey {
override val keyBytes by lazy { signingKey.keyBytes + decryptingKey.keyBytes }
@Transient
override val magic = KeysmagicNumber.defaultUniversalPrivate
/**
* Important! Private key combines signing and decrypting keys, but uses
* it of the decrypting one to be used in keyring.
*/
@Transient
override val id: KeyId = decryptingKey.id
/**
* Corresponding public key able to verify amd encrypt data created by this
* private key.
*/
val publicKey by lazy {
UniversalPublicKey(signingKey.verifyingKey, decryptingKey.publicKey)
}
companion object {
/**
* Generate 2 new random keys (4 key pairs under the hood) to securely signd and
* decrypt data.
*/
fun new() = UniversalPrivateKey(SigningSecretKey.new(), DecryptingSecretKey.new())
}
}

View File

@ -0,0 +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 kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
/**
* Combination of public keys suitable for both encryption and verification. A counterpart
* of the [UniversalPrivateKey], available also as [UniversalPrivateKey.publicKey].
*
* When using [UniversalRing] and [Container], data encrypted with instances og this class
* can be decrypted with rings containing the corresponding [UniversalPrivateKey].
*/
@Serializable
@SerialName("upub")
class UniversalPublicKey(
val verifyingKey: VerifyingPublicKey,
val encryptingKey: EncryptingPublicKey
): UniversalKey(), VerifyingKey by verifyingKey, EncryptingKey by encryptingKey{
override val keyBytes by lazy { verifyingKey.keyBytes + encryptingKey.keyBytes }
@Transient
override val magic = KeysmagicNumber.defaultUniversalPublic
/**
* Important! Private key combines signing and decrypting keys, but uses
* it of the decrypting one to be used in keyring.
*/
@Transient
override val id: KeyId = encryptingKey.id
}

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
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
@ -62,28 +72,34 @@ class UniversalRing(
* Get all keys for the specified id (normally it could be 0, 1 or 2). See [KeyId] about
* matching id keys.
*/
fun keysById(id: KeyId): List<UniversalKey> = allKeys.filter { it.id == id }
fun findById(id: KeyId): List<UniversalKey> = allKeys.filter { it.id == id }
@Deprecated("please replace", replaceWith = ReplaceWith("findById"))
fun keysById(id: KeyId) = findById(id)
/**
* Return sequence of keys that have at least one of the [tags]
*/
fun keysByTags(vararg tags: String) = sequence {
fun findByTags(vararg tags: String) = sequence {
for (e in keyWithTags.entries) {
if (tags.any { it in e.value }) yield(e.key)
}
}
@Deprecated("please replace", replaceWith = ReplaceWith("findByTag"))
fun keysByTag(vararg tags: String) = findByTags(*tags)
/**
* Get the first key of the specified type having the [tag]
*/
inline fun <reified T> keyByTag(tag: String) = keysByTags(tag).first { it is T }
inline fun <reified T> keyByTag(tag: String) = findByTags(tag).first { it is T }
/**
* Get keys of the specified type having any of the specified tags associated.
*/
@Suppress("unused")
inline fun <reified T> keysByAnyTag(vararg tags: String): Sequence<UniversalKey> =
keysByTags(*tags).filter { it is T }
findByTags(*tags).filter { it is T }
/**
* Get all keys with a given id. Note that _matching keys_ have the same id, see [KeyId] for more.
@ -134,6 +150,7 @@ class UniversalRing(
/**
* Add key and tags to the ring. If the key already exists, tags are merged.
*/
@Suppress("unused")
fun add(key: UniversalKey, tags: Collection<String>): UniversalRing =
UniversalRing(keyWithTags + (key to tags.toSet()))

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
interface VerifyingKey: KeyInstance {

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.signature.InvalidSignatureException
@ -5,6 +15,8 @@ import com.ionspin.kotlin.crypto.signature.Signature
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import net.sergeych.bipack.decodeFromBipack
import net.sergeych.mp_tools.decodeBase64Url
/**
* Public key to verify signatures only
@ -26,4 +38,71 @@ class VerifyingPublicKey(override val keyBytes: UByteArray) : UniversalKey(), Ve
override val magic: KeysmagicNumber = KeysmagicNumber.defaultVerifying
override val id by lazy { KeyId(magic, keyBytes, null, true) }
/**
* Create a [Multikey] that requires presence of this or [other] key
*/
infix fun or(other: VerifyingPublicKey) = Multikey(this) or other
/**
* Create a [Multikey] that requires presence of this or [other] key
*/
infix fun or(other: SigningSecretKey) = Multikey(this) or other
/**
* Create a [Multikey] that requires presence of this or [other] key
*/
infix fun or(other: Multikey) = Multikey(this) or other
/**
* Create a [Multikey] that requires presence of this and [other]
*/
infix fun and(other: VerifyingPublicKey) = Multikey(this) and other
/**
* Create a [Multikey] that requires presence of this and [other]
*/
infix fun and(other: SigningSecretKey) = Multikey(this) and other
/**
* Create a [Multikey] that requires presence of this and [other]
*/
infix fun and(other: Multikey) = Multikey(this) and other
companion object {
/**
* Parse any known public key text representation, including what [toString] return (for public keys it is
* possible)
* @throws IllegalArgumentException the public key isn't recognized, in particular [BinaryId.InvalidException]
* if the text is corrupt
*/
fun parse(text: String): VerifyingPublicKey {
val s = text.trim()
fun parseId(t: String): VerifyingPublicKey {
// assume it is an id:
val id = BinaryId.restoreFromString(t)
return if (id.magic == KeysmagicNumber.defaultVerifying.ordinal)
id.asVerifyingKey as VerifyingPublicKey
else throw IllegalArgumentException("Invalid magick: ${id.magic} when parsing[$t]")
}
// 🗝sig#I1po9Y2I7p2aOxeh4nFyGPm3e0YunBEu1Mo-PmIqP84Evg
return when {
s.startsWith("\uD83D\uDDDDver#") -> parseId(s.drop(6))
s.startsWith("ver#") -> parseId(s.drop(4))
s.startsWith("#") -> parseId(s.drop(1))
else -> {
// consider it is serialized key in base64 format
val data = s.decodeBase64Url().asUByteArray()
if (data.size == 32)
VerifyingPublicKey(data)
else if( data.size == 34) {
// raw id
BinaryId(data).asVerifyingKey as VerifyingPublicKey
}else {
runCatching { data.decodeFromBipack<VerifyingPublicKey>() }.getOrNull()
?: kotlin.runCatching { data.decodeFromBipack<UniversalKey>() as VerifyingPublicKey }
.getOrElse { throw IllegalArgumentException("can't parse verifying key") }
}
}
}
}
}
}

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import kotlinx.serialization.Serializable

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
/**

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.pwhash.*
@ -27,12 +37,48 @@ sealed class KDF {
Moderate,
FixedHigher,
Sensitive,
;
/**
* Create [KDF] of the corresponding strength suitable to derive [numberOfKeys] symmetric keys.
*
* Random salt of proper size is used
*/
fun kdfForSize(numberOfKeys: Int,salt: UByteArray = Argon.randomSalt()): KDF =
creteDefault(SymmetricKey.keyLength * numberOfKeys, this, salt)
/**
* Derive multiple keys from the password. Derivation params will be included in the key ids, see
* [SymmetricKey.id] as [KeyId.kdp].
* Random salt of proper size is used
*
* ___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_.
* Having no [PBKD.Params] it would not be possible to recreate the key, as complexity defaults tend
* to change with time.
*/
@Suppress("unused")
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, salt: UByteArray): SymmetricKey = deriveMultiple(password, 1, salt).first()
}
abstract fun derive(password: String): UByteArray
/**
* Derive a single key from the password, same as [deriveMultipleKeys] with `count==1`
*/
abstract fun deriveKey(password: String): UByteArray
fun deriveMultiple(password: String, count: Int): List<SymmetricKey> {
val bytes = derive(password)
/**
* 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.
*/
fun deriveMultipleKeys(password: String, count: Int): List<SymmetricKey> {
val bytes = deriveKey(password)
val ks = SymmetricKey.keyLength
check(ks * count <= bytes.size) { "KDF is too short for $count keys: ${bytes.size} we need ${ks * count}" }
return (0..<count).map {
@ -54,6 +100,7 @@ sealed class KDF {
val keySize: Int,
) : KDF(), Comparable<Argon> {
/**
* Very abstract strength comparison. If a1 > a2 it generally means that its complexity, and, hopefully,
* strength is higher.
@ -81,7 +128,7 @@ sealed class KDF {
}
}
override fun derive(password: String): UByteArray =
override fun deriveKey(password: String): UByteArray =
PasswordHash.pwhash(keySize, password, salt, instructionsComplexity, memComplexity, algorithm.code)
override fun equals(other: Any?): Boolean {
@ -111,6 +158,17 @@ sealed class KDF {
fun randomSalt() = randomUBytes(saltSize)
/**
* Create a deterministic salt suitable fot this KDF from a given text.
*
* We recommend to use random salts stored, and [KeyId] of password-generated keys already
* do it for you. Use this method only if you can't store [KeyId] or salt; it is generally less secure:
* knowing the base text it is possible to understand, for example, that the same derived password was used
* more than once (random salt makes it impossible).
*/
@Suppress("unused")
fun deriveSaltFromString(text: String) = Hash.Blake2b.deriveSalt(text, saltSize)
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" }
@ -135,6 +193,7 @@ sealed class KDF {
268435456,
salt, keySize
)
Moderate -> Argon(
Alg.default,
crypto_pwhash_OPSLIMIT_MODERATE,
@ -142,14 +201,14 @@ sealed class KDF {
salt, keySize
)
Complexity.Sensitive -> Argon(
Sensitive -> Argon(
Alg.default,
crypto_pwhash_OPSLIMIT_SENSITIVE,
crypto_pwhash_MEMLIMIT_SENSITIVE,
salt, keySize
)
Complexity.FixedHigher -> Argon(
FixedHigher -> Argon(
V2id_13,
4UL,
1073741824,
@ -162,10 +221,10 @@ sealed class KDF {
}
companion object {
@Suppress("unused")
fun creteDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF {
return Argon.create(complexity, salt, keySize)
}
}
data class Instance(val kdf: KDF, val password: String)

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
@file:Suppress("unused")
package net.sergeych.crypto2

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
@file:Suppress("unused")
package net.sergeych.crypto2

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.tools
@Suppress("unused")

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.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 ;)

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
@file:Suppress("unused")
package net.sergeych.tools

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.tools
import net.sergeych.bipack.BipackDecoder

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
@file:Suppress("unused")
package net.sergeych.utools

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.utools
import kotlinx.serialization.KSerializer

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
@file:Suppress("unused")
package net.sergeych.utools

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package org.komputing.khash.keccak
import com.ionspin.kotlin.bignum.integer.BigInteger

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
@file:Suppress("unused")
package org.komputing.khash.keccak

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package org.komputing.khash.keccak.extensions
/**

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
@file:Suppress("unused")
package org.komputing.khash.keccak.extensions

View File

@ -0,0 +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.
*/
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 {
@Test
fun testSizes() {
val a = BinaryId.createRandom(5, 4)
// println(a.id.toDump())
// println(pack(a).toDump())
assertEquals(2, a.body.size)
assertEquals(5, a.magic)
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))
}
}

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.encodeToString

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Clock
@ -5,7 +15,10 @@ import net.sergeych.crypto2.Hash
import net.sergeych.crypto2.initCrypto
import kotlin.random.Random
import kotlin.random.nextUBytes
import kotlin.test.*
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 {
@ -46,5 +59,21 @@ class HashTest {
assertContentEquals(Hash.Blake2b.digest(a), p1)
assertContentEquals(Hash.Sha3_384.digest(a), p2)
}
@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)
assertContentEquals(x, y)
assertFalse { x contentEquals z }
assertEquals(x.size, i)
assertEquals(y.size, i)
assertEquals(z.size, i)
}
}
}

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.crypto2.KDF
import net.sergeych.crypto2.initCrypto
@ -38,4 +48,11 @@ class KDFTest {
assertEquals(set2, set1)
}
@Test
fun complexityTest() = runTest{
initCrypto()
val kk = KDF.Complexity.Interactive.kdfForSize(3).deriveMultipleKeys("lala", 3)
assertEquals(3, kk.size)
}
}

View File

@ -1,8 +1,20 @@
/*
* 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
import net.sergeych.crypto2.*
import net.sergeych.tools.bipack
import net.sergeych.tools.biunpack
@ -15,9 +27,9 @@ class KeysTest {
@Test
fun testSigningCreationAndMap() = runTest {
initCrypto()
val (stk,pbk) = SigningSecretKey.generatePair()
val (stk, pbk) = SigningSecretKey.generatePair()
val x = mapOf( stk to "STK!", pbk to "PBK!")
val x = mapOf(stk to "STK!", pbk to "PBK!")
assertEquals("STK!", x[stk])
val s1 = SigningSecretKey(stk.keyBytes)
assertEquals(stk, s1)
@ -52,7 +64,7 @@ class KeysTest {
fun testNonDeterministicSeals() = runTest {
initCrypto()
val data = "Welcome to the crazy new world!".encodeToUByteArray()
val (sk,_) = SigningSecretKey.generatePair()
val (sk, _) = SigningSecretKey.generatePair()
val t = now()
val s1 = Seal.create(sk, data, createdAt = t)
val s2 = Seal.create(sk, data, createdAt = t)
@ -60,11 +72,11 @@ class KeysTest {
val s3 = Seal.create(sk, data, createdAt = t, nonDeterministic = true)
val s4 = Seal.create(sk, data, createdAt = t, nonDeterministic = true)
for( seal in listOf(s1,s2,s3,s4)) {
for (seal in listOf(s1, s2, s3, s4)) {
assertTrue { seal.isValid(data) }
assertTrue { Seal.unpack(seal.packed).isValid(data) }
}
assertFalse { s2bad.isValid(data)}
assertFalse { s2bad.isValid(data) }
assertContentEquals(s1.packed, s2.packed)
assertFalse { s1.packed contentEquals s3.packed }
assertFalse { s4.packed contentEquals s3.packed }
@ -97,7 +109,7 @@ class KeysTest {
assertContentEquals(src, k1.decryptWithNonce(k1.encryptWithNonce(src, nonce), nonce))
assertThrows<DecryptionFailedException> {
val n2 = nonce.copyOf()
n2[4] = n2[4].inv()
n2[4] = n2[4].inv()
k1.decryptWithNonce(k1.encryptWithNonce(src, nonce), n2)
}
@ -173,9 +185,9 @@ class KeysTest {
val (sk0, pk0) = Asymmetric.generateKeys()
// println(sk0.publicKey)
val j = Json { prettyPrint = true}
val j = Json { prettyPrint = true }
val sk1 = j.decodeFromString<SecretKey>(j.encodeToString(sk0))
val sk1 = j.decodeFromString<DecryptingSecretKey>(j.encodeToString(sk0))
assertEquals(sk0, sk1)
assertEquals(pk0, sk1.publicKey)
// println(j.encodeToString(sk1))
@ -204,9 +216,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)
@ -274,6 +286,188 @@ class KeysTest {
assertContentEquals(k.verifyingKey.keyBytes, dk2.id.binaryTag.take(32).toUByteArray())
assertContentEquals(k.verifyingKey.keyBytes, dk1.id.binaryTag.take(32).toUByteArray())
// and restored from id should be the same:
assertEquals( k.verifyingKey, dk2.id.id.asVerifyingKey)
assertEquals(k.verifyingKey, dk2.id.id.asVerifyingKey)
}
}
@Test
fun multiKeyTestSome() = runTest {
initCrypto()
val k1 = SigningSecretKey.new()
val k2 = SigningSecretKey.new()
val k3 = SigningSecretKey.new()
val k4 = SigningSecretKey.new()
val k5 = SigningSecretKey.new()
// val k6 = SigningSecretKey.new()
val mk: Multikey = Multikey.Keys(1, setOf(k1.verifyingKey))
val mk23: Multikey = Multikey.Keys(2, setOf(k1.verifyingKey, k2.verifyingKey, k3.verifyingKey))
val mk13: Multikey = Multikey.Keys(1, setOf(k1.verifyingKey, k2.verifyingKey, k3.verifyingKey))
assertTrue { mk.check(k1.verifyingKey) }
assertFalse { mk.check(k2.verifyingKey) }
assertTrue { mk23.check(k1.verifyingKey, k2.verifyingKey, k4.verifyingKey) }
assertTrue { mk23.check(k3.verifyingKey, k2.verifyingKey, k4.verifyingKey) }
assertFalse { mk23.check(k4.verifyingKey, k2.verifyingKey, k5.verifyingKey) }
assertTrue { mk13.check(k4.verifyingKey, k2.verifyingKey, k5.verifyingKey) }
println(pack(mk23).toDump())
println(pack(mk23).size)
val smk23: Multikey = Multikey.someOf(2, k1.verifyingKey, k2.verifyingKey, k3.verifyingKey)
// val smk13: Multikey = Multikey.Keys(1, setOf(k1.verifyingKey, k2.verifyingKey, k3.verifyingKey))
assertTrue { smk23.check(k1.verifyingKey, k2.verifyingKey, k4.verifyingKey) }
assertTrue { smk23.check(k3.verifyingKey, k2.verifyingKey, k4.verifyingKey) }
assertFalse { smk23.check(k4.verifyingKey, k2.verifyingKey, k5.verifyingKey) }
// assertTrue { smk13.check(k4.verifyingKey, k2.verifyingKey, k5.verifyingKey) }
println(pack(smk23).toDump())
println(pack(smk23).size)
val s1 = k1 or k2 or k3
println(pack(s1).toDump())
println(pack(s1).size)
assertTrue { s1.check(k1.verifyingKey, k2.verifyingKey, k3.verifyingKey) }
assertTrue { s1.check(k1.verifyingKey) }
assertTrue { s1.check(k2.verifyingKey) }
assertTrue { s1.check(k3.verifyingKey) }
assertFalse { s1.check(k4.verifyingKey) }
val s2 = (k1 or k2) and k3
println(pack(s2).toDump())
println(pack(s2).size)
assertTrue { s2.check(k1.verifyingKey, k3.verifyingKey) }
assertTrue { s2.check(k2.verifyingKey, k3.verifyingKey) }
assertTrue { s2.check(k1.verifyingKey, k2.verifyingKey, k3.verifyingKey) }
assertFalse { s2.check(k4.verifyingKey) }
assertFalse { s2.check(k1.verifyingKey) }
assertFalse { s2.check(k2.verifyingKey) }
assertFalse { s2.check(k3.verifyingKey) }
assertFalse { s2.check(k1.verifyingKey, k2.verifyingKey) }
val s3 = (k1 and k2) or k3
println(pack(s3).toDump())
println(pack(s3).size)
assertTrue { s3.check(k1.verifyingKey, k3.verifyingKey) }
assertTrue { s3.check(k3.verifyingKey) }
assertTrue { s3.check(k2.verifyingKey, k1.verifyingKey) }
assertFalse { s3.check(k1.verifyingKey) }
assertFalse { s3.check(k2.verifyingKey) }
assertFalse { s3.check(k1.verifyingKey, k4.verifyingKey) }
}
@Test
fun multiKeyTestAny() = runTest {
initCrypto()
val k1 = SigningSecretKey.new()
val k2 = SigningSecretKey.new()
val k3 = SigningSecretKey.new()
val k4 = SigningSecretKey.new()
val k5 = SigningSecretKey.new()
// val k6 = SigningSecretKey.new()
val mk: Multikey = Multikey.AnyKey
assertTrue { mk.check(k1.verifyingKey) }
assertTrue { mk.check(k2.verifyingKey) }
assertTrue { mk.check(k3.verifyingKey) }
assertTrue { mk.check(k4.verifyingKey) }
assertTrue { mk.check(k5.verifyingKey) }
}
@Test
fun testCombinedKeys() = runTest {
initCrypto()
val k1 = UniversalPrivateKey.new()
val k2 = UniversalPrivateKey.new()
val k3: UniversalPrivateKey = unpack(pack(k1))
assertEquals(k1, k3)
assertEquals(k1.publicKey, k3.publicKey)
assertEquals(k1.signingKey, k3.signingKey)
assertEquals(k1.verifyingKey, k3.verifyingKey)
val k4: UniversalPublicKey = unpack(pack(k1.publicKey))
assertEquals(k1.publicKey, k4)
assertEquals(k1.publicKey.encryptingKey, k4.encryptingKey)
assertEquals(k1.publicKey.verifyingKey, k4.verifyingKey)
val data =
"""We hold these truths to be self-evident, that all men are created equal,
|that they are endowed by their Creator with certain unalienable Rights,
|that among these are Life, Liberty and the pursuit of Happiness."""
.trimMargin()
val kr1 = UniversalRing.from(k1)
val kr2: UniversalRing = UniversalRing.from(k2)
val bytes = Container.encrypt(data, k2.publicKey)
assertNull(Container.decrypt<String>(bytes, kr1))
assertEquals(data, Container.decrypt<String>(bytes, kr2))
}
@Test
fun testEncodedSizes() = runTest {
initCrypto()
val x = SigningSecretKey.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<SigningSecretKey>(y))
assertContentEquals(x.keyBytes, BipackDecoder.decode<SigningSecretKey>(y).keyBytes)
}
@Test
fun testEncodedSizes2() = runTest {
initCrypto()
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<DecryptingSecretKey>(y))
assertContentEquals(x.keyBytes, BipackDecoder.decode<DecryptingSecretKey>(y).keyBytes)
}
@Test
fun testStringRepresentationAndParse() = runTest {
initCrypto()
val k1 = SigningSecretKey.new()
val k2 = k1.verifyingKey
val k3 = DecryptingSecretKey.new()
val k4 = k3.publicKey
val k5 = UniversalPrivateKey.new()
val k6 = k5.publicKey
assertEquals(32, k2.keyBytes.size)
assertContentEquals(k2.keyBytes, k2.id.id.body)
val k7 = SymmetricKey.new()
val k8 = KDF.Complexity.Interactive.derive("super", KDF.Argon.randomSalt())
fun testToString(k: UniversalKey) {
val s = k.toString()
val kx = UniversalKey.parseString(s)
assertEquals(kx::class, k::class)
assertContentEquals(k.keyBytes, kx.keyBytes)
assertEquals(k.id, kx.id)
assertEquals(k, kx)
}
fun testAsString(k: UniversalKey) {
val s = k.asString()
val kx = UniversalKey.parseString(s)
assertEquals(kx::class, k::class)
assertContentEquals(k.keyBytes, kx.keyBytes)
assertEquals(k.id, kx.id)
assertEquals(k, kx)
}
testToString(k2)
testToString(k4)
for( i in listOf(k1, k2, k3, k4, k5, k6, k7, k8)) testAsString(i)
val x = VerifyingPublicKey.parse("I1po9Y2I7p2aOxeh4nFyGPm3e0YunBEu1Mo-PmIqP84Evg")
println(x)
}
}

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.crypto2.PBKD
import net.sergeych.crypto2.initCrypto
@ -33,7 +43,7 @@ class PBKDTest {
assertEquals(i.kdp, kx.id.kdp)
}
val (y1,y2,y3) = k1.id.kdp!!.kdf.deriveMultiple("foobar", 3)
val (y1,y2,y3) = k1.id.kdp!!.kdf.deriveMultipleKeys("foobar", 3)
for( (a,b) in listOf(y1,y2,y3).zip(listOf(k1,k2,k3))) {
assertEquals(a,b)
assertNotNull(a.id.kdp)

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import net.sergeych.crypto2.initCrypto

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
import 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,17 +164,17 @@ 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.keysById(c.id).first())
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.keysById(c.id).first())
assertEquals(c, r1.findById(c.id).first())
}
}

View File

@ -0,0 +1,87 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.bintools.*
import net.sergeych.bipack.decodeFromBipack
import net.sergeych.crypto2.DecryptionFailedException
import net.sergeych.crypto2.EncryptedKVStorage
import net.sergeych.crypto2.SymmetricKey
import net.sergeych.crypto2.initCrypto
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
class StorageTest {
@Test
fun testGetAndSet() = runTest {
initCrypto()
val plain = MemoryKVStorage()
val key = SymmetricKey.new()
val storage = EncryptedKVStorage(plain, key, removeExisting = false)
var hello by storage.optStored<String>()
assertNull(hello)
hello = "world"
assertEquals("world", storage["hello"]?.decodeFromBipack<String>())
println("plain: ${plain.keys}")
assertEquals(setOf("hello"), storage.keys)
var foo by storage.stored("bar")
assertEquals("bar", foo)
foo = "bar2"
// plain.dump()
// storage.dump()
assertEquals(setOf("hello", "foo"), storage.keys)
}
@Test
fun testReEncrypt() = runTest {
initCrypto()
fun test(x: KVStorage) {
val foo by x.stored("1")
val bar by x.stored("2")
val bazz by x.stored("3")
assertEquals("foo", foo)
assertEquals("bar", bar)
assertEquals("bazz", bazz)
}
fun setup(s: KVStorage, k: SymmetricKey): EncryptedKVStorage {
val x = EncryptedKVStorage(s, k, removeExisting = false)
var foo by x.stored("1")
var bar by x.stored("2")
var bazz by x.stored("3")
foo = "foo"
bar = "bar"
bazz = "bazz"
return x
}
val k1 = SymmetricKey.new()
val k2 = SymmetricKey.new()
val plain = MemoryKVStorage()
val s1 = setup(plain, k1)
test(s1)
s1.reEncrypt(k2)
test(s1)
// val s2 = EncryptedKVStorage(plain, k2)
// test(s2)
}
@Test
fun testDeleteExisting() = runTest {
initCrypto()
val plain = MemoryKVStorage()
val c1 = EncryptedKVStorage(plain, SymmetricKey.new(), removeExisting = false) // 1
c1.write("hello", "world")
assertFailsWith<DecryptionFailedException> {
EncryptedKVStorage(plain, SymmetricKey.new(), removeExisting = false) // 2
}
EncryptedKVStorage(plain, SymmetricKey.new(), removeExisting = true) // 2
}
}
@Suppress("unused")
fun KVStorage.dump() {
for (k in keys)
println("$k: ${this[k]?.toDump()}")
}

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.bintools.encodeToHex
import net.sergeych.crypto2.BinaryId

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
import kotlin.test.fail
inline fun <reified T: Throwable>assertThrows(f: ()->Unit): T {

View File

@ -1,3 +1,13 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder