initial commit (moved from divan)

This commit is contained in:
Sergey Chernov 2023-11-11 23:52:18 +03:00
commit 770555024f
56 changed files with 2745 additions and 0 deletions

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

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

View File

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

7
.idea/codeStyles/Project.xml generated Normal file
View File

@ -0,0 +1,7 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<ScalaCodeStyleSettings>
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
</ScalaCodeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

15
.idea/gradle.xml generated Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

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

6
.idea/kotlinc.xml generated Normal file
View File

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

10
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="17 (5)" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

83
build.gradle.kts Normal file
View File

@ -0,0 +1,83 @@
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType
plugins {
kotlin("multiplatform") version "1.9.20"
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.20"
`maven-publish`
}
group = "net.sergeych"
version = "0.1.0-SNAPSHOT"
repositories {
mavenCentral()
mavenLocal()
maven("https://maven.universablockchain.com/")
}
kotlin {
jvm {
jvmToolchain(11)
withJava()
testRuns.named("test") {
executionTask.configure {
useJUnitPlatform()
}
}
}
js(KotlinJsCompilerType.IR) {
browser {
// commonWebpackConfig {
// cssSupport {
// enabled.set(true)
// }
// }
}
}
val hostOs = System.getProperty("os.name")
val isArm64 = System.getProperty("os.arch") == "aarch64"
val isMingwX64 = hostOs.startsWith("Windows")
val nativeTarget = when {
hostOs == "Mac OS X" && isArm64 -> macosArm64("native")
hostOs == "Mac OS X" && !isArm64 -> macosX64("native")
hostOs == "Linux" && isArm64 -> linuxArm64("native")
hostOs == "Linux" && !isArm64 -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
}
sourceSets {
all {
languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi")
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
}
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.0")
api("com.ionspin.kotlin:bignum:0.3.8")
api("net.sergeych:mp_bintools:0.0.6-SNAPSHOT")
api("net.sergeych:mp_stools:1.4.1")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation("org.slf4j:slf4j-simple:2.0.9")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
}
}
val jvmMain by getting
val jvmTest by getting
val jsMain by getting
val jsTest by getting
val nativeMain by getting
val nativeTest by getting
}
}

1
gradle.properties Normal file
View File

@ -0,0 +1 @@
kotlin.code.style=official

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
gradlew vendored Executable file
View File

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

12
settings.gradle.kts Normal file
View File

@ -0,0 +1,12 @@
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
}
rootProject.name = "kiloparsec"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
package net.sergeych.kiloparsec
import kotlin.test.Test
class ClientTest {
@Test
fun testClient() {
// Todo
}
}

View File

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