initial commit (moved from divan)
This commit is contained in:
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 > toIndex</tt>
|
||||
* @throws ArrayIndexOutOfBoundsException if <tt>fromIndex < 0</tt> or
|
||||
* <tt>toIndex > 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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user