Compare commits

...

19 Commits

Author SHA1 Message Date
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
65 changed files with 1695 additions and 58 deletions

1
.gitignore vendored
View File

@ -41,4 +41,5 @@ out/
# Other
.kotlin
.idea
.gigaide
/kotlin-js-store/yarn.lock

View File

@ -8,6 +8,8 @@ All primitives meant to send over the network or store are `kotlinx.serializatio
# 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 +21,7 @@ repositories {
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
}
dependencies {
import("net.sergeych:crypto2:0.5.7")
import("net.sergeych:crypto2:0.7.1-SNAPSHOT")
}
```
@ -34,6 +36,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 +114,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,3 +1,15 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@ -8,17 +20,19 @@ plugins {
}
group = "net.sergeych"
version = "0.5.7"
version = "0.8.1"
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,11 @@ kotlin {
iosArm64()
iosSimulatorArm64()
mingwX64()
// @OptIn(ExperimentalWasmDsl::class)
// wasmJs() //no libsodium bindings yet (strangely)
// val ktor_version = "2.3.6"
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
}
val ktor_version = "2.3.6"
sourceSets {
all {
@ -78,8 +94,21 @@ 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) }
val wasmJsMain by getting {
val wasmJsTargetRegex = Regex(pattern = "wasmJs.*")
configurations.all {
if (wasmJsTargetRegex.containsMatchIn(input = this.name)) {
resolutionStrategy.dependencySubstitution {
substitute(module("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.2"))
.using(module("net.sergeych:multiplatform-crypto-libsodium-bindings:0.9.4-SNAPSHOT"))
.withoutClassifier()
}
}
}
}
}
}

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

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")
@ -30,7 +83,9 @@ open class BinaryId protected constructor (
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 ) }
@ -41,6 +96,11 @@ open class BinaryId protected constructor (
VerifyingPublicKey(body)
}
/**
* Try to recnstruct a [PublicKey] from [id] bytes. For such keys, [PublicKey.id] and [SecretKey.id]
* are made from public key bytes so it could be restored from such an ID
*
*/
val asPublicKey: PublicKey by lazy {
if( magic != KeysmagicNumber.defaultAssymmetric.ordinal)
throw InvalidException("It is not a veryfing key: magic=$magic, required ${KeysmagicNumber.defaultAssymmetric.ordinal}")
@ -50,6 +110,11 @@ open class BinaryId protected constructor (
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())

View File

@ -0,0 +1,100 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import kotlinx.serialization.Serializable
import net.sergeych.bintools.decodeHex
import net.sergeych.bintools.encodeToHex
import kotlin.math.min
/**
* Bytes sequence with comparison, concatenation, and string representation,
* could be used as hash keys for pure binary values, etc.
*/
@Suppress("unused")
@Serializable
class ByteChunk(val data: UByteArray): Comparable<ByteChunk> {
val size: Int get() = data.size
/**
* Per-byte comparison also of different length. From two chunks
* of different size but equal beginning, the shorter is considered
* the smaller.
*/
override fun compareTo(other: ByteChunk): Int {
val limit = min(size, other.size)
for( i in 0 ..< limit) {
val own = data[i]
val their = other.data[i]
if( own < their) return -1
else if( own > their) return 1
}
if( size < other.size ) return -1
if( size > other.size ) return 1
return 0
}
/**
* Equal chunks means content equality.
*/
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ByteChunk) return false
return data contentEquals other.data
}
/**
* Content-based hash code
*/
override fun hashCode(): Int {
return data.contentHashCode()
}
/**
* hex representation of data
*/
override fun toString(): String = base64Url
/**
* Hex encoded data
*/
val hex by lazy { data.encodeToHex() }
val base64Url by lazy { data.encodeToBase64Url() }
/**
* human-readable dump
*/
val dump by lazy { data.toDump() }
/**
* Concatenate two chunks and return new one
*/
operator fun plus(other: ByteChunk): ByteChunk = ByteChunk(data + other.data)
fun toByteArray(): ByteArray = data.asByteArray()
fun toUByteArray(): UByteArray = data
companion object {
fun fromHex(hex: String): ByteChunk = ByteChunk(hex.decodeHex().asUByteArray())
fun random(sizeInBytes: Int=16) = randomUBytes(sizeInBytes).toChunk()
}
}
private fun UByteArray.toChunk(): ByteChunk = ByteChunk(this)
@Suppress("unused")
private fun ByteArray.toChunk(): ByteChunk = ByteChunk(this.asUByteArray())
@Suppress("unused")
fun ByteArray.asChunk() = ByteChunk(toUByteArray())
fun UByteArray.asChunk(): ByteChunk = ByteChunk(this)

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,6 +30,8 @@ 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
@ -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.
*/
@ -152,6 +165,11 @@ sealed class Container {
var authorisedByKey: PublicKey? = 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
}
@ -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) {
@ -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

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,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.generichash.GenericHash
@ -7,7 +17,7 @@ import kotlinx.coroutines.flow.Flow
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 +33,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 +122,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 +159,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
@ -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 [PublicKey] 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,8 +1,21 @@
/*
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
*
* You may use, distribute and modify this code under the
* terms of the private license, which you must obtain from the author
*
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
* real dot sergeych at gmail.
*/
package net.sergeych.crypto2
import 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
/**
* The public for public-key encryption. It encrypts messages that can only be decrypted with corresponding
@ -71,4 +84,40 @@ class PublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingK
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): PublicKey {
val s = text.trim()
fun parseId(t: String): PublicKey{
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)
PublicKey(data)
else {
runCatching { data.decodeFromBipack<PublicKey>() }.getOrNull()
?: kotlin.runCatching { data.decodeFromBipack<UniversalKey>() as PublicKey }
.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

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

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

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 [PublicKey] and [SecretKey].
*
* 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:
@ -34,12 +55,34 @@ sealed class UniversalKey: KeyInstance {
companion object {
fun newSecretKey() = SecretKey.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#") ->
PublicKey.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: SecretKey
) : 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(), SecretKey.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: PublicKey
): 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
@ -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,47 @@ 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): KDF = creteDefault(SymmetricKey.keyLength * numberOfKeys, this)
/**
* 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): List<SymmetricKey> =
kdfForSize(count).deriveMultipleKeys(password, count)
/**
* Derive single key from password, same as [deriveMultiple] with count=1.
*/
fun derive(password: String): SymmetricKey = deriveMultiple(password, 1).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 +99,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 +127,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 +157,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 +192,7 @@ sealed class KDF {
268435456,
salt, keySize
)
Moderate -> Argon(
Alg.default,
crypto_pwhash_OPSLIMIT_MODERATE,
@ -142,14 +200,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 +220,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.toDump
import net.sergeych.bipack.BipackEncoder
import net.sergeych.crypto2.BinaryId
import net.sergeych.crypto2.ByteChunk
import net.sergeych.crypto2.initCrypto
import kotlin.test.Test
import kotlin.test.assertContentEquals
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.toByteArray().size)
assertEquals(3, x.toUByteArray().size)
println(BipackEncoder.encode(x).toDump())
assertEquals(4, BipackEncoder.encode(x).size)
assertContentEquals(BipackEncoder.encode(x.toByteArray()), 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,7 +185,7 @@ 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))
assertEquals(sk0, 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 = SecretKey.new()
// println("key bytes: ${x.keyBytes.size}:\n${x.keyBytes.toDump()}")
val y = BipackEncoder.encode(x)
// println("packed: ${y.size}: ${y.toDump()}")
assertTrue { x.keyBytes.size + 5 > y.size }
assertEquals(x, BipackDecoder.decode<SecretKey>(y))
assertContentEquals(x.keyBytes, BipackDecoder.decode<SecretKey>(y).keyBytes)
}
@Test
fun testStringRepresentationAndParse() = runTest {
initCrypto()
val k1 = SigningSecretKey.new()
val k2 = k1.verifyingKey
val k3 = SecretKey.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")
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
@ -157,14 +167,14 @@ class RingTest {
assertEquals(a, r1.findKey<SecretKey>(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.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

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