initial commit (moved from divan)

This commit is contained in:
sergeych committed 2023-11-11 23:53:37 +03:00
commit 770555024f
56 files changed
+2745

No files matched your search

@@ -0,0 +1,27 @@
package net.sergeych.crypto
import com.ionspin.kotlin.crypto.LibsodiumInitializer
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
private var isReady = false
private val readyAccess = Mutex()
/**
* Library initialization: should be called before all other calls.
* It is safe and with little performance penalty to call it multiple times.
*/
suspend fun initCrypto() {
// faster to check with no lock
if( !isReady) {
readyAccess.withLock {
// recheck with lock, it could be ready by now
if( !isReady ) {
LibsodiumInitializer.initialize()
isReady = true
}
}
}
}
@@ -0,0 +1,81 @@
package net.sergeych.crypto
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
/**
* Multi-signed data box. Use [SignedBox.invoke] to easily create
* instances and [SignedBox.plus] to add more signatures (signing keys), and
* [SignedBox.contains] to check for a specific key signature presence.
*
* It is serializable and checks integrity on deserialization. If any of seals does not
* match the signed [message], it throws [IllegalSignatureException] _on deserialization_.
* E.g., if you have it deserialized, it is ok, check it contains all needed keys among
* signers.
*
* __The main constructor is used for deserializing only__. Don't use it directly unless you
* know what you are doing as it may be dangerous.Use one of the above to create or change it.
*/
@Serializable
class SignedBox(
val message: UByteArray,
private val seals: List<Seal>,
@Transient
private val checkOnInit: Boolean = true
) {
/**
* If this instance is not signed by a given key, return new instance signed also by this
* key, or return unchanged (same) object if it is already signed by this key; you
* _can't assume it always returns a copied object!_
*/
operator fun plus(key: Key.Signing): SignedBox =
if (key.verifying in this) this
else SignedBox(message, seals + Seal.create(key, message), false)
/**
* Check that it is signed with a specified key.
*/
operator fun contains(verifyingKey: Key.Verifying): Boolean {
return seals.any { it.key == verifyingKey }
}
init {
if (seals.isEmpty()) throw IllegalArgumentException("there should be at least one seal")
if (checkOnInit) {
if (!seals.all { it.verify(message) }) throw IllegalSignatureException()
}
}
/**
* A key + signature pair for [SignedBox] boxes, usually you don't use it
* directly bug call [SignedBox] constructor or [SignedBox.plus] to
* add seals.
*/
@Serializable
data class Seal(val key: Key.Verifying, val signature: UByteArray) {
fun verify(message: UByteArray): Boolean = key.verify(signature, message)
companion object {
fun create(key: Key.Signing, message: UByteArray): Seal {
return Seal(key.verifying, key.sign(message))
}
}
}
companion object {
/**
* Create a new instance with a specific data sealed by one or more
* keys. At least one key is required to disallow providing not-signed
* instances, e.g. [SignedBox] is guaranteed to be properly sealed when
* successfully instantiated.
*
* @param data a message to sign
* @param keys a list of keys to sign with, should be at least one key.
* @throws IllegalArgumentException if keys are not specified.
*/
operator fun invoke(data: UByteArray, vararg keys: Key.Signing): SignedBox =
SignedBox(data, keys.map { Seal.create(it, data) }, false)
}
}
@@ -0,0 +1,7 @@
package net.sergeych.crypto
import net.sergeych.bintools.CRC
fun isValidContrail(data: UByteArray): Boolean = CRC.crc8(data.copyOfRange(1, data.size)) == data[0]
fun createContrail(data: UByteArray): UByteArray = ubyteArrayOf(CRC.crc8(data)) + data
@@ -0,0 +1,75 @@
package net.sergeych.crypto
import com.ionspin.kotlin.crypto.signature.InvalidSignatureException
import com.ionspin.kotlin.crypto.signature.Signature
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.sergeych.crypto.Key.Signing
/**
* Keys in general: public, secret and later symmetric too.
* Keys could be compared to each other for equality and used
* as a Map keys (not sure about js).
*
* Use [Signing.pair] to create new keys.
*/
@Serializable
sealed class Key {
abstract val packed: UByteArray
override fun equals(other: Any?): Boolean {
return other is Key && other.packed contentEquals packed
}
override fun hashCode(): Int {
return packed.contentHashCode()
}
override fun toString(): String = packed.encodeToBase64Url()
/**
* Public key to verify signatures only
*/
@Serializable
@SerialName("pvk")
class Verifying(override val packed: UByteArray) : Key() {
/**
* Verify the signature and return true if it is correct.
*/
fun verify(signature: UByteArray, message: UByteArray): Boolean = try {
Signature.verifyDetached(signature, message, packed)
true
} catch (_: InvalidSignatureException) {
false
}
override fun toString(): String = "Pub:${super.toString()}"
}
/**
* Secret key to sign only
*/
@Serializable
@SerialName("ssk")
class Signing(override val packed: UByteArray) : Key() {
val verifying: Verifying by lazy {
Verifying(Signature.ed25519SkToPk(packed))
}
fun sign(message: UByteArray): UByteArray = Signature.detached(message, packed)
override fun toString(): String = "Sct:${super.toString()}"
companion object {
data class Pair(val signing: Signing, val verifying: Verifying)
fun pair(): Pair {
val p = Signature.keypair()
return Pair(Signing(p.secretKey), Verifying(p.publicKey))
}
}
}
}
class IllegalSignatureException: RuntimeException("signed data is tampered or signature is corrupted")
@@ -0,0 +1,85 @@
@file:Suppress("unused")
package net.sergeych.crypto
import com.ionspin.kotlin.crypto.secretbox.SecretBox
import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_NONCEBYTES
import com.ionspin.kotlin.crypto.util.LibsodiumRandom
import kotlinx.serialization.Serializable
import net.sergeych.bintools.toDataSource
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder
class DecryptionFailedException : RuntimeException("can't encrypt: wrong key or tampered message")
@Serializable
data class WithNonce(
val cipherData: UByteArray,
val nonce: UByteArray,
)
@Serializable
data class WithFill(
val data: UByteArray,
val safetyFill: UByteArray? = null
) {
constructor(data: UByteArray, fillSize: Int) : this(data, randomBytes(fillSize))
}
fun randomBytes(n: Int): UByteArray = if (n > 0) LibsodiumRandom.buf(n) else ubyteArrayOf()
fun randomBytes(n: UInt): UByteArray = if (n > 0u) LibsodiumRandom.buf(n.toInt()) else ubyteArrayOf()
/**
* Uniform random in `0 ..< max` range
*/
fun randomUInt(max: UInt) = LibsodiumRandom.uniform(max)
fun randomUInt(max: Int) = LibsodiumRandom.uniform(max.toUInt())
fun <T: Comparable<T>>T.limit(range: ClosedRange<T>) = when {
this < range.start -> range.start
this > range.endInclusive -> range.endInclusive
else -> this
}
fun <T: Comparable<T>>T.limitMax(max: T) = if( this < max ) this else max
fun <T: Comparable<T>>T.limitMin(min: T) = if( this > min ) this else min
fun randomNonce(): UByteArray = randomBytes(crypto_secretbox_NONCEBYTES)
/**
* Secret-key encrypt with authentication.
* Generates random nonce and add some random fill to protect
* against some analysis attacks. Nonce is included in the result. To be
* used with [decrypt].
* @param secretKey a _secret_ key, see [SecretBox.keygen()] or like.
* @param plain data to encrypt
* @param fillSize number of random fill data to add. Use random value or default.
*/
fun encrypt(
secretKey: UByteArray,
plain: UByteArray,
fillSize: Int = randomUInt((plain.size * 3 / 10).limitMin(3)).toInt()
): UByteArray {
val filled = BipackEncoder.encode(WithFill(plain, fillSize))
val nonce = randomNonce()
val encrypted = SecretBox.easy(filled.toUByteArray(), nonce, secretKey)
return BipackEncoder.encode(WithNonce(encrypted, nonce)).toUByteArray()
}
/**
* Decrypt a secret-key-based message, normally encrypted with [encrypt].
* @throws DecryptionFailedException if the key is wrong or a message is tampered with (MAC
* check failed).
*/
fun decrypt(secretKey: UByteArray, cipher: UByteArray): UByteArray {
val wn: WithNonce = BipackDecoder.decode(cipher.toDataSource())
try {
return BipackDecoder.decode<WithFill>(
SecretBox.openEasy(wn.cipherData, wn.nonce, secretKey).toDataSource()
).data
}
catch(_: com.ionspin.kotlin.crypto.secretbox.SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey) {
throw DecryptionFailedException()
}
}
@@ -0,0 +1,8 @@
package net.sergeych.crypto
import net.sergeych.bintools.toDump
import net.sergeych.mp_tools.encodeToBase64Url
fun UByteArray.toDump(wide: Boolean = false) = toByteArray().toDump(wide)
fun UByteArray.encodeToBase64Url(): String = toByteArray().encodeToBase64Url()
@@ -0,0 +1,40 @@
package net.sergeych.kiloparsec
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import net.sergeych.bintools.toDataSource
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder
import net.sergeych.utools.unpack
/**
* Typesafe command definition. Command is a universal entity in Divan: it is used
* in node-2-node protocols and client API, and most importantly in calling smart contract
* methods. This is essentially a Kotlin binding to typesafe serialize command calls and
* deserialize results.
*/
class Command<A, R>(
val name: String,
val argsSerializer: KSerializer<A>,
val resultSerializer: KSerializer<R>
) {
@Serializable
data class Call(val name: String,val serializedArgs: UByteArray)
fun packCall(args: A): UByteArray = BipackEncoder.encode(
Call(name, BipackEncoder.encode(argsSerializer, args).toUByteArray())
).toUByteArray()
fun unpackResult(packedResult: UByteArray): R =
unpack(resultSerializer, packedResult)
suspend fun exec(packedArgs: UByteArray, handler: suspend (A) -> R): UByteArray =
BipackEncoder.encode(
resultSerializer,
handler(BipackDecoder.decode(packedArgs.toDataSource(), argsSerializer))
).toUByteArray()
companion object {
fun unpackCall(packedCall: UByteArray): Call = BipackDecoder.decode(packedCall.toDataSource())
}
}
@@ -0,0 +1,34 @@
package net.sergeych.kiloparsec
import kotlinx.serialization.KSerializer
import kotlinx.serialization.serializer
import kotlin.reflect.KProperty
/**
* delegate returning function that creates a [Command] in the current context which by default has the name of
* the property.
*/
inline fun <reified A, reified R> command(overrideName: String? = null): CommandDelegate<A, R> {
return CommandDelegate(
serializer<A>(),
serializer<R>(),
overrideName
)
}
/**
* Delegate to create [Command] via property
*/
class CommandDelegate<A, R>(
private val argsSerializer: KSerializer<A>,
private val resultSerializer: KSerializer<R>,
private val overrideName: String? = null
) {
operator fun getValue(nothing: Nothing?, property: KProperty<*>): Command<A, R> {
return Command(
overrideName ?: property.name,
argsSerializer,
resultSerializer
)
}
}
@@ -0,0 +1,14 @@
package net.sergeych.kiloparsec
/**
* Minimal data to create kiloparsec connection: transport device and a new session object.
*/
data class KiloConnectionData<S>(
val device: Transport.Device,
val session: S
)
/**
* callback that creates new [Transport.Device] and session objects for Kiloparsec connections.
*/
typealias ConnectionDataFactory<S> = suspend ()->KiloConnectionData<S>
@@ -0,0 +1,139 @@
package net.sergeych.kiloparsec
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import net.sergeych.crypto.Key
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.Loggable
import net.sergeych.mp_logger.debug
import net.sergeych.mp_logger.exception
import net.sergeych.mp_tools.globalLaunch
/**
* The auto-connecting client that reconnects to the kiloparsec server
* and maintain connection state flow. Client factory launches a disconnected
* set of coroutines to support automatic reconnection, so you _must_ [close]
* it manually when it is not needed, otherwise it will continue to reconnect.
*/
class KiloClient<S>(
localInterface: KiloInterface<S>,
secretKey: Key.Signing? = null,
connectionDataFactory: ConnectionDataFactory<S>,
) : RemoteInterface,
Loggable by LogTag("CLIF") {
val _state = MutableStateFlow(false)
/**
* State flow that shows the current state of an auto-connecting client. Use it
* to authenticate a client on connection restore, for example.
*/
@Suppress("unused")
val state = _state.asStateFlow()
private var deferredClient = CompletableDeferred<KiloClientConnection<S>>()
private val job =
globalLaunch {
debug { "starting connector" }
while (isActive) {
try {
debug { "getting connection" }
val kc = connectionDataFactory()
debug { "get device and session" }
val client = KiloClientConnection(localInterface, kc,secretKey)
deferredClient.complete(client)
client.run {
_state.value = false
}
debug { "client run finished" }
} catch (_: RemoteInterface.ClosedException) {
debug { "remote closed" }
} catch (_: CancellationException) {
debug { "cancelled" }
} catch (t: Throwable) {
exception { "unexpected exception" to t }
}
_state.value = false
if (deferredClient.isActive)
deferredClient = CompletableDeferred()
}
}
fun close() {
job.cancel()
}
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R = deferredClient.await().call(cmd, args)
/**
* Current session token. This is a per-connection unique random value same on the client and server part so
* it could be used as a nonce to pair MITM and like attacks, be sure that the server is actually
* working, etc.
*/
suspend fun token() = deferredClient.await().token()
/**
* Remote party shared key ([Key.Verifying]]), could be used ti ensure server is what we expected and
* there is no active MITM attack.
*
* Non-null value means the key was successfully authenticated, null means remote party did not provide
* a key. Connection is established either with a properly authenticated key or no key at all.
*/
@Suppress("unused")
suspend fun remoteId() = deferredClient.await().remoteId()
companion object {
class Builder<S>() {
private var interfaceBuilder: (KiloInterface<S>.() -> Unit)? = null
private var sessionBuilder: (() -> S) = {
@Suppress("UNCHECKED_CAST")
Unit as S
}
private var connectionBuilder: (suspend () -> Transport.Device)? = null
var secretIdKey: Key.Signing? = null
/**
* Build local command implementations (remotely callable ones), exception
* class handlers, etc.
*/
fun local(f: KiloInterface<S>.() -> Unit) {
interfaceBuilder = f
}
/**
* Create a new session object, otherwise Unit session will be used
*/
fun session(f: () -> S) {
sessionBuilder = f
}
fun connect(f: suspend () -> Transport.Device) {
connectionBuilder = f
}
internal fun build(): KiloClient<S> {
val i = KiloInterface<S>()
interfaceBuilder?.let { i.it() }
val connector = connectionBuilder ?: throw IllegalArgumentException("connect handler was not set")
return KiloClient(i,secretIdKey) {
KiloConnectionData(connector(),sessionBuilder())
}
}
}
/**
* Call the secure remote command when the secure connection is established. Note that
* it might fail on disconnect. We do not automatically repeat command on disconnect
* as actual services might need identification on reconnecting.
*/
operator fun <S> invoke(f: Builder<S>.() -> Unit): KiloClient<S> {
return Builder<S>().also { it.f() }.build()
}
}
}
@@ -0,0 +1,102 @@
package net.sergeych.kiloparsec
import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
import kotlinx.coroutines.*
import net.sergeych.crypto.Key
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.Loggable
import net.sergeych.mp_logger.debug
import net.sergeych.mp_logger.info
import net.sergeych.utools.pack
private var clientIds = 0
class KiloClientConnection<S>(
private val clientInterface: LocalInterface<KiloScope<S>>,
private val device: Transport.Device,
private val session: S,
private val secretIdKey: Key.Signing? = null,
) : RemoteInterface, Loggable by LogTag("KPC:${++clientIds}") {
constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, secretIdKey: Key.Signing? = null)
: this(localInterface, connection.device, connection.session, secretIdKey)
private val kiloRemoteInterface = CompletableDeferred<KiloRemoteInterface<S>>()
private val deferredParams = CompletableDeferred<KiloParams<S>>()
suspend fun remoteId(): Key.Verifying? = deferredParams.await().remoteIdentity
/**
* Run the client, blocking until the device is closed, or some critical exception
* will stop the transport, or the calling scope will be canceled.
* Cancelling the scope where server is running is a preferred way to stop the client.
*/
suspend fun run(onConnectedStateChanged: ((Boolean) -> Unit)? = null) {
coroutineScope {
var job: Job? = null
try {
// in parallel: keys and connection
val deferredKeyPair = async { KeyExchange.keypair() }
debug { "opening device" }
debug { "got a transport device $device" }
// client transport has no dedicated commands (unlike the server's),
// it is a calling party:
val l0Interface = KiloL0Interface(clientInterface, deferredParams)
val transport = Transport(device, l0Interface, Unit)
job = launch { transport.run() }
debug { "transport started" }
val pair = deferredKeyPair.await()
debug { "keypair ready" }
val serverHe = transport.call(L0Request, Handshake(1u, pair.publicKey))
val sk = KeyExchange.clientSessionKeys(pair.publicKey, pair.secretKey, serverHe.publicKey)
var params = KiloParams(false, transport, sk, session, null, this@KiloClientConnection)
// Check ID if any
serverHe.serverSharedKey?.let { k ->
if (serverHe.signature == null)
throw RemoteInterface.SecurityException("missing signature")
if (!k.verify(serverHe.signature, params.token))
throw RemoteInterface.SecurityException("wrong signature")
params = params.copy(remoteIdentity = k)
}
transport.call(
L0ClientId, params.encrypt(
pack(
ClientIdentity(
secretIdKey?.verifying,
secretIdKey?.sign(params.token)
)
)
)
)
deferredParams.complete(params)
kiloRemoteInterface.complete(
KiloRemoteInterface(deferredParams, clientInterface)
)
onConnectedStateChanged?.invoke(true)
job.join()
} catch (x: CancellationException) {
info { "client is cancelled" }
} catch (x: RemoteInterface.ClosedException) {
info { "connection closed by remote" }
} finally {
onConnectedStateChanged?.invoke(false)
job?.cancel()
device.apply { runCatching { close() } }
}
}
}
suspend fun token() = deferredParams.await().token
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R =
kiloRemoteInterface.await().call(cmd, args)
}
@@ -0,0 +1,20 @@
package net.sergeych.kiloparsec
/**
* The local interface to provice functions, register errors for Kiloparsec users. Use it
* with [KiloClient], [KiloClientConnection], [KiloServerConnection], etc.
*
* BAse implementation registers relevant exceptions.
*/
class KiloInterface<S> : LocalInterface<KiloScope<S>>() {
init {
registerError { RemoteInterface.UnknownCommand() }
registerError { RemoteInterface.ClosedException(it) }
registerError { RemoteInterface.SecurityException(it) }
registerError { RemoteInterface.InvalidDataException(it) }
registerError { RemoteInterface.RemoteException(it) }
registerError { IllegalStateException() }
registerError { IllegalArgumentException(it) }
}
}
@@ -0,0 +1,36 @@
package net.sergeych.kiloparsec
import kotlinx.coroutines.CompletableDeferred
import net.sergeych.utools.pack
/**
* This class is not normally used directly. This is a local interface that supports
* secure transport command layer (encrypted calls/results) to work with [KiloRemoteInterface].
*
* It is recommended to use [KiloClientConnection] and [KiloServerConnection] instead.
*/
internal class KiloL0Interface<T>(
private val clientInterface: LocalInterface<KiloScope<T>>,
private val deferredParams: CompletableDeferred<KiloParams<T>>,
): LocalInterface<Unit>() {
init {
// local interface uses the same session as a client:
addErrorProvider(clientInterface)
on(L0Call) {
val params = deferredParams.await()
val call = Command.unpackCall(params.decrypt(it))
// tricky part: we need to encrypt a result or error: we can't allow default
// error transport (as it is not encrypted), so we re-use transport blocks here:
val result: Transport.Block = try {
Transport.Block.Response(
0u,
clientInterface.execute(params.scope, call.name, call.serializedArgs)
)
} catch (t: Throwable) {
clientInterface.encodeError(0u, t)
}
params.encrypt(pack(result))
}
}
}
@@ -0,0 +1,136 @@
package net.sergeych.kiloparsec
import com.ionspin.kotlin.crypto.keyexchange.KeyExchangeSessionKeyPair
import com.ionspin.kotlin.crypto.secretbox.SecretBox
import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_NONCEBYTES
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
import kotlinx.serialization.Serializable
import net.sergeych.bintools.toDataSource
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder
import net.sergeych.crypto.DecryptionFailedException
import net.sergeych.crypto.Key
import net.sergeych.crypto.randomBytes
import net.sergeych.crypto.randomUInt
import net.sergeych.tools.ProtectedOp
import net.sergeych.utools.pack
import net.sergeych.utools.unpack
import org.komputing.khash.keccak.Keccak
import org.komputing.khash.keccak.KeccakParameter
import kotlin.math.roundToInt
/**
* Parameters used in secured local and remote interfaces, etc. Actual values
* are calculated in [KiloServerConnection] and [KiloClientConnection] by
* exchanging keys, then used to encrypt/decrypt calls and results and create
* [KiloScope] with a proper session object.
*
* __important: parameters' calculation algorithms are different on the server and
* client side, so you need to provide proper [isServer] value!__
*/
data class KiloParams<S>(
val isServer: Boolean,
val transport: RemoteInterface,
val sessionKeyPair: KeyExchangeSessionKeyPair,
val scopeSession: S,
val remoteIdentity: Key.Verifying?,
val remoteTransport: RemoteInterface
) {
@Serializable
data class Package(
val nonce: ULong,
val encryptedMessage: UByteArray,
)
@Serializable
data class FilledData(
val message: UByteArray,
val fill: UByteArray,
)
private var nonce = 0UL
val scope: KiloScope<S> by lazy {
object : KiloScope<S> {
override val session = scopeSession
override val remote: RemoteInterface = remoteTransport
override val sessionToken: UByteArray = token
override val remoteIdentity: Key.Verifying? = this@KiloParams.remoteIdentity
}
}
val token: UByteArray by lazy {
val base = if (isServer) sessionKeyPair.sendKey + sessionKeyPair.receiveKey
else sessionKeyPair.receiveKey + sessionKeyPair.sendKey
Keccak.digest(
base.toByteArray(), KeccakParameter.KECCAK_256
).toUByteArray().sliceArray(0..<crypto_secretbox_NONCEBYTES)
}
private val sendBase by lazy {
Keccak.digest(
sessionKeyPair.sendKey.toByteArray(), KeccakParameter.KECCAK_256
).toUByteArray().sliceArray(0..<crypto_secretbox_NONCEBYTES)
}
private val receiveBase by lazy {
Keccak.digest(
sessionKeyPair.receiveKey.toByteArray(), KeccakParameter.KECCAK_256
).toUByteArray().sliceArray(0..<crypto_secretbox_NONCEBYTES)
}
private inline fun encodeNonce(base: UByteArray, nonce: ULong): UByteArray {
val result = base.copyOf()
var x = nonce
var i = 0
while (x > 0u) {
result[i] = result[i] xor (x and 0xFFu).toUByte()
x = x shr 8
i++
}
return result
}
private inline fun encodeSendNonce(nonce: ULong): UByteArray = encodeNonce(sendBase, nonce)
private inline fun encodeReceiveNonce(nonce: ULong): UByteArray = encodeNonce(receiveBase, nonce)
fun encrypt(plainText: String): UByteArray = encrypt(plainText.encodeToUByteArray())
private val proptectedOp = ProtectedOp()
/**
* Encrypt using send keys and proper nonce
*/
fun encrypt(message: UByteArray, fillFactor: Float = 0f): UByteArray {
val fill: UByteArray = if (fillFactor > 0f)
randomBytes(randomUInt((message.size * fillFactor).roundToInt()))
else
ubyteArrayOf()
val withFill = BipackEncoder.encode(FilledData(message, fill)).toUByteArray()
val n = proptectedOp { nonce++ }
return pack(
Package(n, SecretBox.easy(withFill, encodeSendNonce(n), sessionKeyPair.sendKey))
)
}
fun decryptString(cipherText: UByteArray): String = decrypt(cipherText).decodeFromUByteArray()
fun decrypt(encryptedMessage: UByteArray): UByteArray {
val p: Package = BipackDecoder.decode(encryptedMessage.toDataSource())
try {
return unpack<FilledData>(
SecretBox.openEasy(
p.encryptedMessage,
encodeReceiveNonce(p.nonce),
sessionKeyPair.receiveKey
)
).message
} catch (_: com.ionspin.kotlin.crypto.secretbox.SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey) {
throw DecryptionFailedException()
}
}
}
@@ -0,0 +1,33 @@
package net.sergeych.kiloparsec
import kotlinx.coroutines.CompletableDeferred
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.Loggable
import net.sergeych.utools.unpack
private var L1IdCounter = 0
// todo: We don't need it deferred here
class KiloRemoteInterface<S>(
private val deferredParams: CompletableDeferred<KiloParams<S>>,
private val clientInterface: LocalInterface<KiloScope<S>>,
) : RemoteInterface, Loggable by LogTag("L1TR:${++L1IdCounter}") {
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R {
val params = deferredParams.await()
val block: Transport.Block = unpack(
params.decrypt(
params.transport.call(L0Call, params.encrypt(cmd.packCall(args)))
)
)
return when (block) {
is Transport.Block.Response -> {
cmd.unpackResult(block.packedResult)
}
is Transport.Block.Error -> clientInterface.decodeAndThrow(block)
else -> throw RemoteInterface.Exception("unexpected block type: $block")
}
}
}
@@ -0,0 +1,42 @@
package net.sergeych.kiloparsec
import net.sergeych.crypto.Key
/**
* Scope for Kiloparsec client/server commands execution, contain per-connection specific data. The scope
* is used to call command implementation you add to the [KiloInterface] when constructing [KiloClient]
* [KiloClientConnection] or [KiloServerConnection].
*/
interface KiloScope<S> {
/**
* Session object. Any data provided by the caller when creating a new connection
*/
val session: S
/**
* The secure (L1) interface to call remote commands
*/
val remote: RemoteInterface
/**
* Unique per-connection token which is the same on the server and client side, though is never
* transmitted (derived from Diffie-Hellman key exchange or like process). It can be used as a
* safe nonce or seed to test connection integrity without sending check data over the channel.
*/
val sessionToken: UByteArray
/**
* If the remote part has provided a secret key, e.g., gave non-null [Key.Signing] on construction,
* the kiloparsec checks it in the MITM-safe way and provides its [Key.Verifying] shared key here.
* Knowing a remote party shared key, it is possible to be sure that the connection is made directly
* to this party with no middle point intruders.
*
* Note that if the key was provided but authentication failed, the connection __will not be established__,
* throwing [RemoteInterface.SecurityException].
*
* In spite of the above said, which means, non-null value in this field means the key is authorized, but
* It is up to the caller to ensure it is expected key of the remote party.
*/
val remoteIdentity: Key.Verifying?
}
@@ -0,0 +1,104 @@
package net.sergeych.kiloparsec
import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
import kotlinx.coroutines.CompletableDeferred
import net.sergeych.crypto.Key
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.Loggable
import net.sergeych.mp_logger.debug
import net.sergeych.utools.unpack
private var serverIds: Int = 0
/**
* A single kiloparsec server connection. Create it and call [run] and wait until it ends, or cancel
* parent coroutine context to stop.
*
* @param clientInterface local commands, serializable exceptions declarations, etc. usually it is an
* reusable object between connection
* @param device connected device to operate over
* @param session local session object. Use Unit for no session
*/
class KiloServerConnection<S>(
private val clientInterface: KiloInterface<S>,
private val device: Transport.Device,
private val session: S,
private val serverSigningKey: Key.Signing? = null
) : RemoteInterface, Loggable by LogTag("SRV${++serverIds}") {
/**
* Shortcut to construct with [KiloConnectionData] intance
*/
@Suppress("unused")
constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, serverSigningKey: Key.Signing? = null)
: this(localInterface, connection.device, connection.session, serverSigningKey)
private val kiloRemoteInterface = CompletableDeferred<KiloRemoteInterface<S>>()
/**
* Run the transport processing loop. This method suspends and only returns when the connection
* is closed, normally or exceptionally. Cancel the scope where it was called to safely
* stop processing, otherwise close the [device].
*/
suspend fun run() {
val deferredParams = CompletableDeferred<KiloParams<S>>()
val deferredTransport = CompletableDeferred<Transport<*>>()
val l0Interface = KiloL0Interface(clientInterface, deferredParams).apply {
var params: KiloParams<S>? = null
on(L0Request) {
val sk = KeyExchange.serverSessionKeys(
pair.publicKey, pair.secretKey, it.publicKey
)
params = KiloParams(
true,
deferredTransport.await(),
sk,
session,
null,
this@KiloServerConnection
)
var verifying: Key.Verifying? = null
var signature: UByteArray? = null
if( serverSigningKey != null ) {
verifying = serverSigningKey.verifying
signature = serverSigningKey.sign(params!!.token)
}
Handshake(1u, pair.publicKey, verifying, signature)
}
on(L0ClientId) {
var p = params ?: throw RemoteInterface.ClosedException("wrong handshake sequence")
val ci = unpack<ClientIdentity>(p.decrypt(it))
if( ci.clientIdKey != null ) {
if( ci.signature == null )
throw RemoteInterface.SecurityException("missing signature")
if( !ci.clientIdKey.verify(ci.signature, params!!.token))
throw RemoteInterface.SecurityException("wrong signature")
p = p.copy(remoteIdentity = ci.clientIdKey)
params = p
}
deferredParams.complete(p)
kiloRemoteInterface.complete(
KiloRemoteInterface(deferredParams, clientInterface)
)
}
}
val transport = Transport(device, l0Interface, Unit)
deferredTransport.complete(transport)
kiloRemoteInterface.complete(KiloRemoteInterface(deferredParams,clientInterface))
debug { "starintg the transport"}
transport.run()
debug { "server transport finished" }
}
companion object {
val pair = KeyExchange.keypair()
}
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R {
return kiloRemoteInterface.await().call(cmd, args)
}
}
@@ -0,0 +1,89 @@
package net.sergeych.kiloparsec
import net.sergeych.utools.firstNonNull
import kotlin.reflect.KClass
private typealias RawCommandHandler<C> = suspend (C, UByteArray) -> UByteArray
open class LocalInterface<S> {
private val commands = mutableMapOf<String, RawCommandHandler<S>>()
/**
* New session creator. Rarely needed directlym it can be used for delegation
* of local interfaces.
*/
// var sessionMaker: suspend () -> S = {
// @Suppress("UNCHECKED_CAST")
// Unit as? S ?: throw IllegalStateException("newSession handler is not set")
// }
// private set
// /**
// * Builder-style method to create session. Sets the [sessionMaker] actually.
// */
// fun createSession(sessionMaker: suspend () -> S) {
// this.sessionMaker = sessionMaker
// }
/**
* Define a command body to be executed locally.
*/
fun <A, R> on(command: Command<A, R>, handler: suspend S.(A) -> R) {
commands[command.name] = { cxt, args ->
command.exec(args) { handler(cxt, it) }
}
}
suspend fun execute(
scope: S,
name: String,
packedArgs: UByteArray,
): UByteArray =
(commands[name] ?: throw RemoteInterface.UnknownCommand())
.invoke(scope, packedArgs)
private val errorByClass = mutableMapOf<KClass<*>, String>()
private val errorBuilder = mutableMapOf<String, (String, UByteArray?) -> Throwable>()
fun <T : Throwable> registerError(
klass: KClass<T>, code: String = klass.simpleName!!,
exceptionBuilder: (String, UByteArray?) -> T,
) {
errorByClass[klass] = code
errorBuilder[code] = exceptionBuilder
}
inline fun <reified T : Throwable> registerError(
noinline exceptionBuilder: (String) -> T,
) {
registerError(T::class) { msg, _ -> exceptionBuilder(msg) }
}
val errorProviders = mutableListOf<LocalInterface<*>>()
fun <I : LocalInterface<*>> addErrorProvider(provider: I) {
errorProviders += provider
}
fun getErrorCode(t: Throwable): String? =
errorByClass[t::class] ?: errorProviders.firstNonNull { it.getErrorCode(t) }
fun encodeError(forId: UInt, t: Throwable): Transport.Block.Error =
getErrorCode(t)?.let { Transport.Block.Error(forId, it, t.message) }
?: Transport.Block.Error(forId, "UnknownError", t.message)
open fun getErrorBuilder(code: String): ((String, UByteArray?) -> Throwable)? =
errorBuilder[code] ?: errorProviders.firstNonNull { it.getErrorBuilder(code) }
fun decodeError(tbe: Transport.Block.Error): Throwable =
getErrorBuilder(tbe.code)?.invoke(tbe.message, tbe.extra)
?: RemoteInterface.RemoteException(tbe)
fun decodeAndThrow(tbe: Transport.Block.Error): Nothing {
throw decodeError(tbe)
}
}
@@ -0,0 +1,50 @@
package net.sergeych.kiloparsec
/**
* Kiloparsec interface to call remote methods.
*
* It is used as for secure layer (L1) as for underlying
* service layer L0. When using [KiloClientConnection], [KiloServerConnection] and [KiloClient]
* it always implement secure layer, L1.
*/
interface RemoteInterface {
/**
* General channel exception
*/
open class Exception(text: String, reason: Throwable? = null) : RuntimeException(text, reason)
/**
* Is thrown when the channel is closed, in an attempt to execute a command, also to all pending
* calls (see [call]).
*/
open class ClosedException(t: String = "connection is closed") : Exception(t)
open class SecurityException(t: String = "invalid remote id and signature") : ClosedException(t)
open class InvalidDataException(msg: String="invalid data, can't unpack") : Exception(msg)
/**
* Remote call caused an exception thrown while executing it in the remote party. Note that it
* does not mean the channel state is bad or closed.
*/
open class RemoteException(
val code: String,
val text: String = "remote exception: $code",
val extra: UByteArray? = null
) : Exception(text) {
constructor(remoteError: Transport.Block.Error) : this(remoteError.code, remoteError.message, remoteError.extra)
}
/**
* Command is not supported by the remote party
*/
class UnknownCommand : RemoteException("UnknownCommand")
suspend fun <R> call(cmd: Command<Unit, R>): R = call(cmd, Unit)
/**
* Call the remote procedure with specified args and return its result
*/
suspend fun <A, R> call(cmd: Command<A, R>, args: A): R
}
@@ -0,0 +1,244 @@
package net.sergeych.kiloparsec
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.serializer
import net.sergeych.crypto.toDump
import net.sergeych.kiloparsec.Transport.Device
import net.sergeych.mp_logger.*
import net.sergeych.utools.pack
import net.sergeych.utools.unpack
/**
* Divan channel that operates some block [Device] exporting a given [localInterface]
* to remote callers. [LocalInterface] allows session managing, transmitting exceptions
* in a scure and multiplatform way and provide local command execution (typed RPC)
*/
class Transport<S>(
private val device: Device,
private val localInterface: LocalInterface<S>,
private val commandContext: S,
) : Loggable by LogTag("TR:$device"), RemoteInterface {
/**
* Channel operates using an abstract device, that performs binary block exchange implementing
* this interface.
*/
interface Device {
/**
* Input blocks. When the device is disconnected, it should send one null to this channel
* to notify the owner. When [close] is called, the channel should be closed.
*/
val input: ReceiveChannel<UByteArray?>
/**
* Send a binary block to a remote party where it should be received and put into [input]
* channel. If the device is closed, it should close this channel, also by [close].
*/
val output: SendChannel<UByteArray>
/**
* Close input and output and free any resources. The output channel should be flushed if
* possible. This method must not throw exceptions.
*/
suspend fun close()
}
@Serializable(TransportBlockSerializer::class)
sealed class Block {
@Serializable
data class Call(val id: UInt, val name: String, val packedArgs: UByteArray) : Block() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Call) return false
if (id != other.id) return false
if (name != other.name) return false
if (!(packedArgs contentEquals other.packedArgs)) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + packedArgs.contentHashCode()
return result
}
}
@Serializable
data class Response(val forId: UInt, val packedResult: UByteArray) : Block()
@Serializable
data class Error(val forId: UInt, val code: String, val text: String? = null, val extra: UByteArray? = null) :
Block() {
val message by lazy { text ?: "remote exception: $code" }
}
}
private val access = Mutex()
private var lastId = 0u
private val calls = mutableMapOf<UInt, CompletableDeferred<UByteArray>>()
var isClosed: Boolean = false
/**
* Send a call block for a command and packed args and return packed result if it is not an error
* @throws RemoteInterface.RemoteException if the remote call caused an exception. Normally use [call] instead.
* @throws RemoteInterface.ClosedException
*/
private suspend fun sendCallBlock(name: String, packedArgs: UByteArray): UByteArray {
if (isClosed) throw RemoteInterface.ClosedException()
val b: Block
val deferred = CompletableDeferred<UByteArray>()
// We need to shield calls and lastID with mutex, but nothing more:
access.withLock {
if (isClosed) throw RemoteInterface.ClosedException()
b = Block.Call(++lastId, name, packedArgs)
calls[b.id] = deferred
}
// now we have mutex freed so we can call:
val r = device.output.trySend(pack(b).also { debug { ">>>\n${it.toDump()}" } })
if (!r.isSuccess) deferred.completeExceptionally(RemoteInterface.ClosedException())
// it returns packed result or throws a proper error:
return deferred.await()
}
/**
* Call the remote procedure with specified args and return its result
*/
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R {
val result = sendCallBlock(cmd.name, pack(cmd.argsSerializer, args))
return unpack(cmd.resultSerializer, result)
}
/**
* Start running the transport. This function suspends until the transport is closed
* normally or by error. If you need to cancel it prematurely, cancel the coroutine
* it is started in. This approach allows using transport with lifespan connected to the
* calling coroutine which greatly simplifies its usage in popular asyn platofrms like
* a ktor client and server, compose multiplatform, etc.
*/
suspend fun run() {
coroutineScope {
debug { "awaiting incoming blocks" }
while (isActive && !isClosed) {
try {
device.input.receive()?.let { packed ->
debug { "<<<\n${packed.toDump()}" }
val b = unpack<Block>(packed)
debug { "<<$ $b" }
debug { "access state: ${access.isLocked}" }
when (b) {
is Block.Error -> access.withLock {
val error = localInterface.decodeError(b)
warning { "decoded error: ${error::class.simpleName}: $error" }
calls.remove(b.forId)?.completeExceptionally(localInterface.decodeError(b))
?: warning { "error handler not found for ${b.forId}" }
info { "error processed"}
}
is Block.Response -> access.withLock {
calls.remove(b.forId)?.let {
debug { "activating wait handle for ${b.forId}" }
it.complete(b.packedResult)
}
?: warning { "wait handle not found for ${b.forId}" }
}
is Block.Call -> launch {
try {
send(
Block.Response(
b.id,
localInterface.execute(commandContext, b.name, b.packedArgs)
)
)
} catch (x: RemoteInterface.ClosedException) {
// strange case: handler throws closed?
error { "not supported: command handler for $b has thrown ClosedException" }
send(Block.Error(b.id, "UnexpectedException", x.message))
} catch (x: RemoteInterface.RemoteException) {
send(Block.Error(b.id, x.code, x.text, x.extra))
} catch (t: Throwable) {
send(Block.Error(b.id, "UnknownError", t.message))
}
.also { debug { "command executed: ${b.name}" } }
}
}
} ?: run {
debug { "remote channel close received" }
isClosed = true
}
} catch (_: CancellationException) {
info { "loop is cancelled" }
isClosed = true
} catch (t: Throwable) {
exception { "channel closed on error" to t }
info { "isa? $isActive / $isClosed" }
runCatching { device.close() }
isClosed = true
}
}
access.withLock {
isClosed = true
for (c in calls.values) c.completeExceptionally(RemoteInterface.ClosedException())
calls.clear()
}
debug { "no more active: $isActive / ${calls.size}" }
}
info { "exiting transport loop" }
}
private suspend fun send(block: Block) {
device.output.send(pack(block))
}
}
object TransportBlockSerializer : KSerializer<Transport.Block> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("TransportBlock", PrimitiveKind.INT)
override fun serialize(encoder: Encoder, value: Transport.Block) {
when (value) {
is Transport.Block.Call -> {
encoder.encodeByte(0)
encoder.encodeSerializableValue(serializer<Transport.Block.Call>(), value)
}
is Transport.Block.Error -> {
encoder.encodeByte(1)
encoder.encodeSerializableValue(serializer<Transport.Block.Error>(), value)
}
is Transport.Block.Response -> {
encoder.encodeByte(2)
encoder.encodeSerializableValue(serializer<Transport.Block.Response>(), value)
}
}
}
override fun deserialize(decoder: Decoder): Transport.Block =
when( val id = decoder.decodeByte().toInt()) {
0 -> decoder.decodeSerializableValue(serializer<Transport.Block.Call>())
1 -> decoder.decodeSerializableValue(serializer<Transport.Block.Error>())
2 -> decoder.decodeSerializableValue(serializer<Transport.Block.Response>())
else -> throw RemoteInterface.InvalidDataException("wrong block type: $id")
}
}
@@ -0,0 +1,18 @@
package net.sergeych.kiloparsec
import kotlinx.serialization.Serializable
import net.sergeych.crypto.Key
// L0 commands - key exchange and check:
@Serializable
data class Handshake(val version: UInt, val publicKey: UByteArray,
val serverSharedKey: Key.Verifying? = null,
val signature: UByteArray? = null)
@Serializable
data class ClientIdentity(val clientIdKey: Key.Verifying?, val signature: UByteArray?)
// Level 0 command: request key exchange
internal val L0Request by command<Handshake, Handshake>()
internal val L0ClientId by command<UByteArray, Unit>()
internal val L0Call by command<UByteArray,UByteArray>()
@@ -0,0 +1,21 @@
package net.sergeych.tools
/**
* Multiplatform interface to perform a regular (not suspend) operation
* protected by a platform mutex (where necessary). Get real implementation
* with [ProtectedOp]
*/
interface ProtectedOpImplementation {
/**
* Call [f] iin mutually exclusive mode, it means that only one invocation
* can be active at a time, all the rest are waiting until the current operation
* will finish.
*/
operator fun <T>invoke(f: ()->T): T
}
/**
* Get the platform-depended implementation of a mutex-protected operation.
*/
expect fun ProtectedOp(): ProtectedOpImplementation
@@ -0,0 +1,12 @@
package net.sergeych.utools
/**
* Scan the collection and return the first non-null result of the [predicate] on it.
* If all the elements give null with predicate call, returns null.
*
* Note that collection is scanned only to the first non-null predicate result.
*/
fun <T,R>Collection<T>.firstNonNull(predicate: (T)->R?): R? {
for( x in this ) predicate(x)?.let { return it }
return null
}
@@ -0,0 +1,46 @@
package net.sergeych.utools
import kotlinx.serialization.KSerializer
import kotlinx.serialization.serializer
import net.sergeych.bintools.toDataSource
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder
/**
* Effectively pack anyk nullable object. The result could be effectively packed
* in turn as a part of a more complex structure.
*
* To avoid packing non-null mark,
* we use a zero-size array, which, if in turn encoded, packs into a single
* zero byte. Thus, we avoid extra byte spending for unnecessary null
* check.
*/
inline fun <reified T> pack(element: T?): UByteArray = pack(serializer<T>(), element)
/**
* Unpack nullable data packed with [pack]
*/
inline fun <reified T: Any?> unpack(encoded: UByteArray): T =
unpack(serializer<T>(), encoded)
/**
* Effectively pack anyk nullable object. The result could be effectively packed
* in turn as a part of a more complex structure.
*
* To avoid packing non-null mark,
* we use a zero-size array, which, if in turn encoded, packs into a single
* zero byte. Thus, we avoid extra byte spending for unnecessary null
* check.
*/
fun <T>pack(serializer: KSerializer<T>, element: T?): UByteArray =
if (element == null) ubyteArrayOf()
else BipackEncoder.encode(serializer,element).toUByteArray()
/**
* Unpack nullable data packed with [pack]
*/
@Suppress("UNCHECKED_CAST")
fun <T: Any?> unpack(serializer: KSerializer<T>, encoded: UByteArray): T =
if (encoded.isEmpty()) null as T
else BipackDecoder.decode(encoded.toByteArray().toDataSource(),serializer)
@@ -0,0 +1,12 @@
@file:Suppress("unused")
package net.sergeych.utools
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
fun now(): Instant = Clock.System.now()
fun nowToSeconds(): Instant = Clock.System.now().truncateToSeconds()
fun Instant.truncateToSeconds(): Instant =
Instant.fromEpochSeconds(toEpochMilliseconds()/1000)
@@ -0,0 +1,183 @@
package org.komputing.khash.keccak
import com.ionspin.kotlin.bignum.integer.BigInteger
import org.komputing.khash.keccak.extensions.fillWith
import kotlin.math.min
object Keccak {
private val BIT_65 = BigInteger.ONE shl (64)
private val MAX_64_BITS = BIT_65 - BigInteger.ONE
fun digest(value: ByteArray, parameter: KeccakParameter): ByteArray {
val uState = IntArray(200)
val uMessage = convertToUInt(value)
var blockSize = 0
var inputOffset = 0
// Absorbing phase
while (inputOffset < uMessage.size) {
blockSize = min(uMessage.size - inputOffset, parameter.rateInBytes)
for (i in 0 until blockSize) {
uState[i] = uState[i] xor uMessage[i + inputOffset]
}
inputOffset += blockSize
if (blockSize == parameter.rateInBytes) {
doF(uState)
blockSize = 0
}
}
// Padding phase
uState[blockSize] = uState[blockSize] xor parameter.d
if (parameter.d and 0x80 != 0 && blockSize == parameter.rateInBytes - 1) {
doF(uState)
}
uState[parameter.rateInBytes - 1] = uState[parameter.rateInBytes - 1] xor 0x80
doF(uState)
// Squeezing phase
val byteResults = mutableListOf<Byte>()
var tOutputLen = parameter.outputLengthInBytes
while (tOutputLen > 0) {
blockSize = min(tOutputLen, parameter.rateInBytes)
for (i in 0 until blockSize) {
byteResults.add(uState[i].toByte().toInt().toByte())
}
tOutputLen -= blockSize
if (tOutputLen > 0) {
doF(uState)
}
}
return byteResults.toByteArray()
}
private fun doF(uState: IntArray) {
val lState = Array(5) { Array(5) { BigInteger.ZERO } }
for (i in 0..4) {
for (j in 0..4) {
val data = IntArray(8)
val index = 8 * (i + 5 * j)
uState.copyInto(data, 0, index, index + data.size)
lState[i][j] = convertFromLittleEndianTo64(data)
}
}
roundB(lState)
uState.fillWith(0)
for (i in 0..4) {
for (j in 0..4) {
val data = convertFrom64ToLittleEndian(lState[i][j])
data.copyInto(uState, 8 * (i + 5 * j))
}
}
}
/**
* Permutation on the given state.
*/
private fun roundB(state: Array<Array<BigInteger>>) {
var lfsrState = 1
for (round in 0..23) {
val c = arrayOfNulls<BigInteger>(5)
val d = arrayOfNulls<BigInteger>(5)
// θ step
for (i in 0..4) {
c[i] = state[i][0].xor(state[i][1]).xor(state[i][2]).xor(state[i][3]).xor(state[i][4])
}
for (i in 0..4) {
d[i] = c[(i + 4) % 5]!!.xor(c[(i + 1) % 5]!!.leftRotate64(1))
}
for (i in 0..4) {
for (j in 0..4) {
state[i][j] = state[i][j].xor(d[i]!!)
}
}
// ρ and π steps
var x = 1
var y = 0
var current = state[x][y]
for (i in 0..23) {
val tX = x
x = y
y = (2 * tX + 3 * y) % 5
val shiftValue = current
current = state[x][y]
state[x][y] = shiftValue.leftRotate64Safely((i + 1) * (i + 2) / 2)
}
// χ step
for (j in 0..4) {
val t = arrayOfNulls<BigInteger>(5)
for (i in 0..4) {
t[i] = state[i][j]
}
for (i in 0..4) {
// ~t[(i + 1) % 5]
val invertVal = t[(i + 1) % 5]!!.xor(MAX_64_BITS)
// t[i] ^ ((~t[(i + 1) % 5]) & t[(i + 2) % 5])
state[i][j] = t[i]!!.xor(invertVal.and(t[(i + 2) % 5]!!))
}
}
// ι step
for (i in 0..6) {
lfsrState = (lfsrState shl 1 xor (lfsrState shr 7) * 0x71) % 256
// pow(2, i) - 1
val bitPosition = (1 shl i) - 1
if (lfsrState and 2 != 0) {
state[0][0] = state[0][0].xor(BigInteger.ONE shl bitPosition)
}
}
}
}
/**
* Converts the given [data] array to an [IntArray] containing UInt values.
*/
private fun convertToUInt(data: ByteArray) = IntArray(data.size) {
data[it].toInt() and 0xFF
}
/**
* Converts the given [data] array containing the little endian representation of a number to a [BigInteger].
*/
private fun convertFromLittleEndianTo64(data: IntArray): BigInteger {
val value = data.map { it.toString(16) }
.map { if (it.length == 2) it else "0$it" }
.reversed()
.joinToString("")
return BigInteger.parseString(value, 16)
}
/**
* Converts the given [BigInteger] to a little endian representation as an [IntArray].
*/
private fun convertFrom64ToLittleEndian(uLong: BigInteger): IntArray {
val asHex = uLong.toString(16)
val asHexPadded = "0".repeat((8 * 2) - asHex.length) + asHex
return IntArray(8) {
((7 - it) * 2).let { pos ->
asHexPadded.substring(pos, pos + 2).toInt(16)
}
}
}
private fun BigInteger.leftRotate64Safely(rotate: Int) = leftRotate64(rotate % 64)
private fun BigInteger.leftRotate64(rotate: Int) = (this shr (64 - rotate)).add(this shl rotate).mod(BIT_65)
}
@@ -0,0 +1,22 @@
@file:Suppress("unused")
package org.komputing.khash.keccak
/**
* Parameters defining the FIPS 202 standard.
*/
enum class KeccakParameter(val rateInBytes: Int,val outputLengthInBytes: Int, val d: Int) {
KECCAK_224(144, 28, 0x01),
KECCAK_256(136, 32, 0x01),
KECCAK_384(104, 48, 0x01),
KECCAK_512(72, 64, 0x01),
SHA3_224(144, 28, 0x06),
SHA3_256(136, 32, 0x06),
SHA3_384(104, 48, 0x06),
SHA3_512(72, 64, 0x06),
SHAKE128(168, 32, 0x1F),
SHAKE256(136, 64, 0x1F)
}
@@ -0,0 +1,42 @@
package org.komputing.khash.keccak.extensions
/**
* Assigns the specified int value to each element of the specified
* range in the specified array of ints. The range to be filled
* extends from index <tt>fromIndex</tt>, inclusive, to index
* <tt>toIndex</tt>, exclusive. (If <tt>fromIndex==toIndex</tt>, the
* range to be filled is empty.)
*
* @param fromIndex the index of the first element (inclusive) to be
* filled with the specified value
* @param toIndex the index of the last element (exclusive) to be
* filled with the specified value
* @param value the value to be stored in all elements of the array
* @throws IllegalArgumentException if <tt>fromIndex &gt; toIndex</tt>
* @throws ArrayIndexOutOfBoundsException if <tt>fromIndex &lt; 0</tt> or
* <tt>toIndex &gt; a.length</tt>
*/
internal fun IntArray.fillWith(value: Int, fromIndex: Int = 0, toIndex: Int = this.size) {
if (fromIndex > toIndex) {
throw IllegalArgumentException(
"fromIndex($fromIndex) > toIndex($toIndex)"
)
}
if (fromIndex < 0) {
throw ArrayIndexOutOfBoundsException(fromIndex)
}
if (toIndex > this.size) {
throw ArrayIndexOutOfBoundsException(toIndex)
}
for (i in fromIndex until toIndex)
this[i] = value
}
/**
* Constructs a new [ArrayIndexOutOfBoundsException]
* class with an argument indicating the illegal index.
* @param index the illegal index.
*/
internal class ArrayIndexOutOfBoundsException(index: Int) : Throwable("Array index out of range: $index")
@@ -0,0 +1,19 @@
@file:Suppress("unused")
package org.komputing.khash.keccak.extensions
import org.komputing.khash.keccak.Keccak
import org.komputing.khash.keccak.KeccakParameter
/**
* Computes the proper Keccak digest of [this] byte array based on the given [parameter]
*/
fun ByteArray.digestKeccak(parameter: KeccakParameter): ByteArray {
return Keccak.digest(this, parameter)
}
/**
* Computes the proper Keccak digest of [this] string based on the given [parameter]
*/
fun String.digestKeccak(parameter: KeccakParameter): ByteArray {
return Keccak.digest(encodeToByteArray(), parameter)
}
+58
View File
@@ -0,0 +1,58 @@
import com.ionspin.kotlin.crypto.secretbox.SecretBox
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
import kotlinx.coroutines.test.runTest
import net.sergeych.crypto.*
import net.sergeych.utools.pack
import net.sergeych.utools.unpack
import kotlin.test.*
class KeysTest {
@Test
fun testCreationAndMap() = runTest {
initCrypto()
val (stk,pbk) = Key.Signing.pair()
val x = mapOf( stk to "STK!", pbk to "PBK!")
assertEquals("STK!", x[stk])
val s1 = Key.Signing(stk.packed)
assertEquals(stk, s1)
assertEquals("STK!", x[s1])
assertEquals("PBK!", x[pbk])
val data = "8 rays dev!".encodeToUByteArray()
val data1 = "8 rays dev!".encodeToUByteArray()
val s = SignedBox.Seal.create(stk, data)
assertTrue(s.verify(data))
data1[0] = 0x01u
assertFalse(s.verify(data1))
val p2 = Key.Signing.pair()
val p3 = Key.Signing.pair()
val ms = SignedBox(data, s1) + p2.signing
// non tampered:
val ms1 = unpack<SignedBox>(pack(ms))
assertContentEquals(data, ms1.message)
assertTrue(pbk in ms1)
assertTrue(p2.verifying in ms1)
assertTrue(p3.verifying !in ms1)
assertThrows<IllegalSignatureException> {
unpack<SignedBox>(pack(ms).also { it[3] = 1u })
}
}
@Test
fun secretEncryptTest() = runTest {
initCrypto()
val key = SecretBox.keygen()
val key1 = SecretBox.keygen()
assertEquals("hello", decrypt(key, encrypt(key, "hello".encodeToUByteArray())).decodeFromUByteArray())
assertThrows<DecryptionFailedException> {
decrypt(key, encrypt(key1, "hello".encodeToUByteArray())).decodeFromUByteArray()
}
}
}
+43
View File
@@ -0,0 +1,43 @@
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import net.sergeych.crypto.initCrypto
import net.sergeych.kiloparsec.Transport
import net.sergeych.utools.nowToSeconds
import net.sergeych.utools.pack
import net.sergeych.utools.unpack
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.microseconds
class PackTest {
inline fun <reified T>check(x: T?) {
assertEquals(x, unpack<T>(pack(x)))
}
@Test
fun testNullPack() = runTest {
initCrypto()
val d = pack("Hello")
assertEquals(6, d.size)
check(1)
check(2L)
check(1.00)
check("hello")
check<String>(null)
check<Long?>(null)
}
@Test
fun testTimePack() = runTest {
initCrypto()
val t1 = nowToSeconds()
val t2 = t1 + 1.microseconds
assertEquals(t1, unpack<Instant>(pack(t2)))
}
@Test
fun packBlocks() {
val b1 = Transport.Block.Call(1u, "foobar", ubyteArrayOf(1u,2u,3u))
val p1 = pack(b1 as Transport.Block)
val b2 = unpack<Transport.Block>(p1)
assertEquals(b1,b2)
}
}
+20
View File
@@ -0,0 +1,20 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.crypto.createContrail
import net.sergeych.crypto.initCrypto
import net.sergeych.crypto.isValidContrail
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ToolsTest {
@Test
fun testContrails() = runTest {
initCrypto()
val c = createContrail(ubyteArrayOf(1u, 2u, 3u, 4u, 5u))
assertEquals(134u, c[0])
assertTrue { isValidContrail(c) }
c[2] = 11u
assertFalse { isValidContrail(c) }
}
}
+288
View File
@@ -0,0 +1,288 @@
import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.test.runTest
import net.sergeych.crypto.Key
import net.sergeych.crypto.initCrypto
import net.sergeych.kiloparsec.command
import net.sergeych.kiloparsec.*
import net.sergeych.mp_logger.Log
import kotlin.test.*
private var dcnt = 0
fun createTestDevice(): Pair<Transport.Device, Transport.Device> {
val p1 = Channel<UByteArray>(256)
val p2 = Channel<UByteArray>(256)
val id = ++dcnt
val d1 = object : Transport.Device {
override val input: ReceiveChannel<UByteArray?> = p1
override val output: SendChannel<UByteArray> = p2
override suspend fun close() {
p2.close()
}
override fun toString(): String {
return "D1:$id"
}
}
val d2 = object : Transport.Device {
override val input: ReceiveChannel<UByteArray?> = p2
override val output: SendChannel<UByteArray> = p1
override suspend fun close() {
p1.close()
}
override fun toString(): String {
return "D2:$id"
}
}
return d1 to d2
}
class TransportTest {
@Test
fun testTransportL0AndEncryption() = runTest {
initCrypto()
val cmdPing by command<String, String>()
val cmdSlow by command<String, String>()
Log.connectConsole()
Log.defaultLevel = Log.Level.DEBUG
val (d1, d2) = createTestDevice()
val l1 = LocalInterface<Unit>().apply {
on(cmdPing) {
"p1: $it"
}
}
val l2 = LocalInterface<Unit>().apply {
on(cmdPing) {
"p2: $it"
}
on(cmdSlow) {
delay(100)
"done"
}
}
val t1 = Transport(d1, l1, Unit)
val t2 = Transport(d2, l2, Unit)
val clip = KeyExchange.keypair()
val serp = KeyExchange.keypair()
val clisk = KeyExchange.clientSessionKeys(clip.publicKey, clip.secretKey, serp.publicKey)
val sersk = KeyExchange.serverSessionKeys(serp.publicKey, serp.secretKey, clip.publicKey)
val pser = KiloParams(true, t1, sersk, Unit, null, t1)
val pcli = KiloParams(false, t2, clisk, Unit, null, t2)
assertContentEquals(pcli.token, pser.token)
assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
assertEquals(pser.decryptString(pcli.encrypt("hello2!")), "hello2!")
assertEquals(pser.decryptString(pcli.encrypt("hello3!")), "hello3!")
assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
// test nonce increment
assertFalse { pcli.encrypt("once") contentEquals pcli.encrypt("once") }
assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
coroutineScope {
val j1 = launch { t1.run() }
val j2 = launch { t2.run() }
launch {
assertThrows<RemoteInterface.ClosedException> {
t1.call(cmdSlow, "foo1")
}
}
assertEquals("p2: foo", t1.call(cmdPing, "foo"))
assertEquals("p1: bar", t2.call(cmdPing, "bar"))
assertEquals("p2: foo", t1.call(cmdPing, "foo"))
j1.cancelAndJoin()
j2.cancelAndJoin()
}
d1.close()
d2.close()
}
@Test
fun testConnection() = runTest {
initCrypto()
val cmdPing by command<String, String>()
val cmdPush by command<String, String>()
val cmdGetToken by command<Unit, UByteArray>()
Log.connectConsole()
// Log.defaultLevel = Log.Level.DEBUG
val (d1, d2) = createTestDevice()
val serverInterface = KiloInterface<String>().apply {
on(cmdPing) {
"pong! [$it]"
}
on(cmdGetToken) {
sessionToken
}
registerError { IllegalStateException() }
registerError { IllegalArgumentException(it) }
}
val kiloServerConnection = KiloServerConnection(serverInterface, d1, "server session")
launch { kiloServerConnection.run() }
val clientInterface = KiloInterface<String>().apply {
on(cmdPush) {
"server push: $it"
}
on(cmdPing) {
"client pong: $it"
}
}
val client = KiloClientConnection(clientInterface, d2, "client session")
launch { client.run() }
assertEquals("pong! [hello]", client.call(cmdPing, "hello"))
assertEquals("pong! [foo]", client.call(cmdPing, "foo"))
assertEquals("client pong: foo", kiloServerConnection.call(cmdPing, "foo"))
assertEquals("server push: bar", kiloServerConnection.call(cmdPush, "bar"))
assertContentEquals(client.token(), client.call(cmdGetToken))
d1.close()
d2.close()
}
@Test
fun testClient() = runTest {
initCrypto()
val cmdPing by command<String, String>()
val cmdPush by command<String, String>()
val cmdGetToken by command<Unit, UByteArray>()
val cmdGetClientId by command<Unit, Key.Verifying?>()
val cmdChainCallServer1 by command<String, String>()
val cmdChainCallClient1 by command<String, String>()
val cmdChainCallServer2 by command<String, String>()
val cmdChainCallClient2 by command<String, String>()
Log.connectConsole()
// Log.defaultLevel = Log.Level.DEBUG
val (d1, d2) = createTestDevice()
val serverId = Key.Signing.pair()
val clientId = Key.Signing.pair()
val serverInterface = KiloInterface<String>().apply {
on(cmdPing) {
"pong! [$it]"
}
on(cmdGetToken) {
sessionToken
}
on(cmdGetClientId) {
remoteIdentity
}
on(cmdChainCallServer1) {
remote.call(cmdChainCallClient1, it + "-s1")
}
on(cmdChainCallServer2) {
remote.call(cmdChainCallClient2, "$it-s2")
}
registerError { IllegalStateException() }
registerError { IllegalArgumentException(it) }
}
val kiloServerConnection = KiloServerConnection(serverInterface, d1, "server session", serverId.signing)
launch { kiloServerConnection.run() }
var cnt = 0
val client = KiloClient {
session { "client session!" }
secretIdKey = clientId.signing
local {
on(cmdPush) {
"server push: $it"
}
on(cmdPing) {
"client pong: $it"
}
on(cmdChainCallClient1) {
remote.call(cmdChainCallServer2,"$it-c1")
}
on(cmdChainCallClient2) { "$it-c2" }
}
connect {
if (cnt++ > 0) {
cancel()
fail("connect called once again")
}
d2
}
}
assertEquals("pong! [hello]", client.call(cmdPing, "hello"))
assertEquals("pong! [foo]", client.call(cmdPing, "foo"))
assertEquals("client pong: foo", kiloServerConnection.call(cmdPing, "foo"))
assertEquals("server push: bar", kiloServerConnection.call(cmdPush, "bar"))
assertEquals("**-s1-c1-s2-c2", client.call(cmdChainCallServer1, "**"))
d1.close()
d2.close()
client.close()
// assertEquals(1, connectionCounter)
}
@Test
fun testAuthentication() = runTest {
initCrypto()
val cmdPing by command<String, String>()
val cmdPush by command<String, String>()
val cmdGetToken by command<Unit, UByteArray>()
Log.connectConsole()
Log.defaultLevel = Log.Level.DEBUG
val (d1, d2) = createTestDevice()
val serverInterface = KiloInterface<String>().apply {
on(cmdPing) {
"pong! [$it]"
}
on(cmdGetToken) {
sessionToken
}
registerError { IllegalStateException() }
registerError { IllegalArgumentException(it) }
}
val kiloServerConnection = KiloServerConnection(serverInterface, d1, "server session")
launch { kiloServerConnection.run() }
var cnt = 0
val client = KiloClient {
session { "client session!" }
local {
on(cmdPush) {
"server push: $it"
}
on(cmdPing) {
"client pong: $it"
}
}
connect {
if (cnt++ > 0) {
cancel()
fail("connect called once again")
}
d2
}
}
assertEquals("pong! [hello]", client.call(cmdPing, "hello"))
assertEquals("pong! [foo]", client.call(cmdPing, "foo"))
assertEquals("client pong: foo", kiloServerConnection.call(cmdPing, "foo"))
assertEquals("server push: bar", kiloServerConnection.call(cmdPush, "bar"))
assertContentEquals(client.call(cmdGetToken), client.token())
client.close()
d1.close()
d2.close()
}
}
+13
View File
@@ -0,0 +1,13 @@
import kotlin.test.fail
inline fun <reified T: Throwable>assertThrows(f: ()->Unit): T {
val name = T::class.simpleName
try {
f()
fail("expected to throw $name but threw nothing")
}
catch(x: Throwable) {
if( x is T ) return x
fail("expected to throw $name but instead threw ${x::class.simpleName}: $x")
}
}
@@ -0,0 +1,6 @@
package net.sergeych.tools
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
// JS targets are inherently single-threaded, so we do noting:
override fun <T> invoke(f: () -> T): T = f()
}
@@ -0,0 +1,8 @@
package net.sergeych.tools
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
private val lock = Object()
override fun <T> invoke(f: () -> T): T {
synchronized(lock) { return f() }
}
}
@@ -0,0 +1,10 @@
package net.sergeych.kiloparsec
import kotlin.test.Test
class ClientTest {
@Test
fun testClient() {
// Todo
}
}
@@ -0,0 +1,13 @@
package net.sergeych.tools
import kotlinx.atomicfu.locks.SynchronizedObject
import kotlinx.atomicfu.locks.synchronized
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
private val lock = SynchronizedObject()
override fun <T> invoke(f: () -> T): T {
synchronized(lock) {
return f()
}
}
}