From f429cfe418b3bb170971532ef2389312c2bd4fce Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 22 Nov 2023 23:55:14 +0300 Subject: [PATCH] missing initial files + publishing --- build.gradle.kts | 18 ++ .../kotlin/net/sergeych/crypto2/InitCrypto.kt | 27 +++ .../kotlin/net/sergeych/crypto2/Seal.kt | 11 ++ .../kotlin/net/sergeych/crypto2/SignedBox.kt | 65 +++++++ .../kotlin/net/sergeych/crypto2/SigningKey.kt | 77 ++++++++ .../kotlin/net/sergeych/crypto2/contrail.kt | 7 + .../kotlin/net/sergeych/crypto2/tools.kt | 111 +++++++++++ .../kotlin/net/sergeych/crypto2/utools.kt | 10 + .../net/sergeych/tools/AtomicCounter.kt | 10 + .../kotlin/net/sergeych/tools/ProtectedOp.kt | 21 ++ .../kotlin/net/sergeych/tools/flow_tools.kt | 22 +++ .../kotlin/net/sergeych/utools/collections.kt | 14 ++ .../kotlin/net/sergeych/utools/packing.kt | 46 +++++ .../kotlin/net/sergeych/utools/time.kt | 12 ++ .../org/komputing/khash/keccak/Keccak.kt | 183 ++++++++++++++++++ .../komputing/khash/keccak/KeccakParameter.kt | 22 +++ .../keccak/extensions/IntArrayExtensions.kt | 42 ++++ .../keccak/extensions/PublicExtensions.kt | 19 ++ src/commonTest/kotlin/KeysTest.kt | 58 ++++++ src/commonTest/kotlin/PackTest.kt | 35 ++++ src/commonTest/kotlin/ToolsTest.kt | 20 ++ src/commonTest/kotlin/assertThrows.kt | 13 ++ .../net/sergeych/tools/ProtectedOp.js.kt | 6 + .../net/sergeych/tools/ProtectedOp.jvm.kt | 8 + .../net/sergeych/tools/ProtectedOp.native.kt | 13 ++ 25 files changed, 870 insertions(+) create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/InitCrypto.kt create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/SignedBox.kt create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/contrail.kt create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/tools.kt create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/utools.kt create mode 100644 src/commonMain/kotlin/net/sergeych/tools/AtomicCounter.kt create mode 100644 src/commonMain/kotlin/net/sergeych/tools/ProtectedOp.kt create mode 100644 src/commonMain/kotlin/net/sergeych/tools/flow_tools.kt create mode 100644 src/commonMain/kotlin/net/sergeych/utools/collections.kt create mode 100644 src/commonMain/kotlin/net/sergeych/utools/packing.kt create mode 100644 src/commonMain/kotlin/net/sergeych/utools/time.kt create mode 100644 src/commonMain/kotlin/org/komputing/khash/keccak/Keccak.kt create mode 100644 src/commonMain/kotlin/org/komputing/khash/keccak/KeccakParameter.kt create mode 100644 src/commonMain/kotlin/org/komputing/khash/keccak/extensions/IntArrayExtensions.kt create mode 100644 src/commonMain/kotlin/org/komputing/khash/keccak/extensions/PublicExtensions.kt create mode 100644 src/commonTest/kotlin/KeysTest.kt create mode 100644 src/commonTest/kotlin/PackTest.kt create mode 100644 src/commonTest/kotlin/ToolsTest.kt create mode 100644 src/commonTest/kotlin/assertThrows.kt create mode 100644 src/jsMain/kotlin/net/sergeych/tools/ProtectedOp.js.kt create mode 100644 src/jvmMain/kotlin/net/sergeych/tools/ProtectedOp.jvm.kt create mode 100644 src/nativeMain/kotlin/net/sergeych/tools/ProtectedOp.native.kt diff --git a/build.gradle.kts b/build.gradle.kts index c4d7126..5315ca1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -87,3 +87,21 @@ kotlin { val nativeTest by getting } } + +publishing { + val mavenToken by lazy { + File("${System.getProperty("user.home")}/.gitea_token").readText() + } + repositories { + maven { + credentials(HttpHeaderCredentials::class) { + name = "Authorization" + value = mavenToken + } + url = uri("https://gitea.sergeych.net/api/packages/SergeychWorks/maven") + authentication { + create("Authorization", HttpHeaderAuthentication::class) + } + } + } +} diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/InitCrypto.kt b/src/commonMain/kotlin/net/sergeych/crypto2/InitCrypto.kt new file mode 100644 index 0000000..21199de --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/InitCrypto.kt @@ -0,0 +1,27 @@ +package net.sergeych.crypto2 + +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 + } + } + } +} + + diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt new file mode 100644 index 0000000..33e5ff5 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt @@ -0,0 +1,11 @@ +package net.sergeych.crypto2 + +import kotlinx.serialization.Serializable + +@Serializable +class Seal( + val publicKey: SigningKey.Public, + val signature: UByteArray +) { + inline fun verify(message: UByteArray) = publicKey.verify(signature, message) +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SignedBox.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SignedBox.kt new file mode 100644 index 0000000..9431d1e --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SignedBox.kt @@ -0,0 +1,65 @@ +package net.sergeych.crypto2 + +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, + @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: SigningKey.Secret): SignedBox = + if (key.publicKey in this) this + else SignedBox(message, seals + key.seal(message), false) + + /** + * Check that it is signed with a specified key. + */ + operator fun contains(publicKey: SigningKey.Public): Boolean { + return seals.any { it.publicKey == publicKey } + } + + init { + if (seals.isEmpty()) throw IllegalArgumentException("there should be at least one seal") + if (checkOnInit) { + if (!seals.all { it.verify(message) }) throw IllegalSignatureException() + } + } + + + 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: SigningKey.Secret): SignedBox = + SignedBox(data, keys.map { it.seal(data) }, false) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt new file mode 100644 index 0000000..cd38676 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt @@ -0,0 +1,77 @@ +package net.sergeych.crypto2 + +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.crypto2.SigningKey.Secret + +/** + * 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 [Secret.pair] to create new keys. + */ +@Serializable +sealed class SigningKey { + abstract val packed: UByteArray + + override fun equals(other: Any?): Boolean { + return other is SigningKey && 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("p") + class Public(override val packed: UByteArray) : SigningKey() { + /** + * 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("s") + class Secret(override val packed: UByteArray) : SigningKey() { + + val publicKey: Public by lazy { + Public(Signature.ed25519SkToPk(packed)) + } + + fun sign(message: UByteArray): UByteArray = Signature.detached(message, packed) + + fun seal(message: UByteArray): Seal = Seal(this.publicKey, sign(message)) + override fun toString(): String = "Sct:${super.toString()}" + + companion object { + data class Pair(val signing: Secret, val aPublic: Public) + + fun pair(): Pair { + val p = Signature.keypair() + return Pair(Secret(p.secretKey), Public(p.publicKey)) + } + } + } +} + +class IllegalSignatureException: RuntimeException("signed data is tampered or signature is corrupted") diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/contrail.kt b/src/commonMain/kotlin/net/sergeych/crypto2/contrail.kt new file mode 100644 index 0000000..abfd8d9 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/contrail.kt @@ -0,0 +1,7 @@ +package net.sergeych.crypto2 + +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 \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/tools.kt b/src/commonMain/kotlin/net/sergeych/crypto2/tools.kt new file mode 100644 index 0000000..cded97d --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/tools.kt @@ -0,0 +1,111 @@ +@file:Suppress("unused") + +package net.sergeych.crypto2 + +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.coroutines.channels.ReceiveChannel +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)) +} + +suspend fun readVarUnsigned(input: ReceiveChannel): UInt { + var result = 0u + var cnt = 0 + while(true) { + val b = input.receive().toUInt() + result = (result shl 7) or (b and 0x7fu) + if( (b and 0x80u) != 0u ) { + return result + } + if( ++cnt > 5 ) throw IllegalArgumentException("overflow while decoding varuint") + } +} + +fun encodeVarUnsigned(value: UInt): UByteArray { + val result = mutableListOf() + var rest = value + do { + val mask = if( rest <= 0x7fu ) 0x80u else 0u + result.add( (mask or (rest and 0x7fu)).toUByte() ) + rest = rest shr 7 + } while(rest != 0u) + return result.toUByteArray() +} + + +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.limit(range: ClosedRange) = when { + this < range.start -> range.start + this > range.endInclusive -> range.endInclusive + else -> this +} + +fun >T.limitMax(max: T) = if( this < max ) this else max +fun >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( + SecretBox.openEasy(wn.cipherData, wn.nonce, secretKey).toDataSource() + ).data + } + catch(_: com.ionspin.kotlin.crypto.secretbox.SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey) { + throw DecryptionFailedException() + } +} diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/utools.kt b/src/commonMain/kotlin/net/sergeych/crypto2/utools.kt new file mode 100644 index 0000000..5ff80d8 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/utools.kt @@ -0,0 +1,10 @@ +@file:Suppress("unused") + +package net.sergeych.crypto2 + +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() \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/tools/AtomicCounter.kt b/src/commonMain/kotlin/net/sergeych/tools/AtomicCounter.kt new file mode 100644 index 0000000..b5fc64b --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/tools/AtomicCounter.kt @@ -0,0 +1,10 @@ +package net.sergeych.tools + +@Suppress("unused") +class AtomicCounter(initialValue: Long = 0) { + private val op = ProtectedOp() + var value: Long = initialValue + private set + + fun incrementAndGet(): Long = op { ++value } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/tools/ProtectedOp.kt b/src/commonMain/kotlin/net/sergeych/tools/ProtectedOp.kt new file mode 100644 index 0000000..be9720c --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/tools/ProtectedOp.kt @@ -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 invoke(f: ()->T): T +} + + +/** + * Get the platform-depended implementation of a mutex-protected operation. + */ +expect fun ProtectedOp(): ProtectedOpImplementation \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/tools/flow_tools.kt b/src/commonMain/kotlin/net/sergeych/tools/flow_tools.kt new file mode 100644 index 0000000..9f4aefc --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/tools/flow_tools.kt @@ -0,0 +1,22 @@ +@file:Suppress("unused") + +package net.sergeych.tools + +import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +/** + * suspend until the flow produces the value to which the + * predicate returns true + */ +suspend fun Flow.waitFor(predicate: (T)->Boolean) { + coroutineScope { + launch { + collect { + if( predicate(it) ) cancel() + } + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/utools/collections.kt b/src/commonMain/kotlin/net/sergeych/utools/collections.kt new file mode 100644 index 0000000..d27dea5 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/utools/collections.kt @@ -0,0 +1,14 @@ +@file:Suppress("unused") + +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 Collection.firstNonNull(predicate: (T)->R?): R? { + for( x in this ) predicate(x)?.let { return it } + return null +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/utools/packing.kt b/src/commonMain/kotlin/net/sergeych/utools/packing.kt new file mode 100644 index 0000000..0b5ac85 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/utools/packing.kt @@ -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 pack(element: T?): UByteArray = pack(serializer(), element) + +/** + * Unpack nullable data packed with [pack] + */ +inline fun unpack(encoded: UByteArray): T = + unpack(serializer(), 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 pack(serializer: KSerializer, element: T?): UByteArray = + if (element == null) ubyteArrayOf() + else BipackEncoder.encode(serializer,element).toUByteArray() + + +/** + * Unpack nullable data packed with [pack] + */ +@Suppress("UNCHECKED_CAST") +fun unpack(serializer: KSerializer, encoded: UByteArray): T = + if (encoded.isEmpty()) null as T + else BipackDecoder.decode(encoded.toByteArray().toDataSource(),serializer) diff --git a/src/commonMain/kotlin/net/sergeych/utools/time.kt b/src/commonMain/kotlin/net/sergeych/utools/time.kt new file mode 100644 index 0000000..6c0ae50 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/utools/time.kt @@ -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) \ No newline at end of file diff --git a/src/commonMain/kotlin/org/komputing/khash/keccak/Keccak.kt b/src/commonMain/kotlin/org/komputing/khash/keccak/Keccak.kt new file mode 100644 index 0000000..6a45600 --- /dev/null +++ b/src/commonMain/kotlin/org/komputing/khash/keccak/Keccak.kt @@ -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() + 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>) { + var lfsrState = 1 + for (round in 0..23) { + val c = arrayOfNulls(5) + val d = arrayOfNulls(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(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) +} diff --git a/src/commonMain/kotlin/org/komputing/khash/keccak/KeccakParameter.kt b/src/commonMain/kotlin/org/komputing/khash/keccak/KeccakParameter.kt new file mode 100644 index 0000000..57570e9 --- /dev/null +++ b/src/commonMain/kotlin/org/komputing/khash/keccak/KeccakParameter.kt @@ -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) +} diff --git a/src/commonMain/kotlin/org/komputing/khash/keccak/extensions/IntArrayExtensions.kt b/src/commonMain/kotlin/org/komputing/khash/keccak/extensions/IntArrayExtensions.kt new file mode 100644 index 0000000..c36c21a --- /dev/null +++ b/src/commonMain/kotlin/org/komputing/khash/keccak/extensions/IntArrayExtensions.kt @@ -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 fromIndex, inclusive, to index + * toIndex, exclusive. (If fromIndex==toIndex, 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 fromIndex > toIndex + * @throws ArrayIndexOutOfBoundsException if fromIndex < 0 or + * toIndex > a.length + */ +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") diff --git a/src/commonMain/kotlin/org/komputing/khash/keccak/extensions/PublicExtensions.kt b/src/commonMain/kotlin/org/komputing/khash/keccak/extensions/PublicExtensions.kt new file mode 100644 index 0000000..d68f972 --- /dev/null +++ b/src/commonMain/kotlin/org/komputing/khash/keccak/extensions/PublicExtensions.kt @@ -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) +} diff --git a/src/commonTest/kotlin/KeysTest.kt b/src/commonTest/kotlin/KeysTest.kt new file mode 100644 index 0000000..e2ebcae --- /dev/null +++ b/src/commonTest/kotlin/KeysTest.kt @@ -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.crypto2.* +import net.sergeych.utools.pack +import net.sergeych.utools.unpack +import kotlin.test.* + +class KeysTest { + @Test + fun testCreationAndMap() = runTest { + initCrypto() + val (stk,pbk) = SigningKey.Secret.pair() + + val x = mapOf( stk to "STK!", pbk to "PBK!") + assertEquals("STK!", x[stk]) + val s1 = SigningKey.Secret(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 = stk.seal(data) + assertTrue(s.verify(data)) + + data1[0] = 0x01u + assertFalse(s.verify(data1)) + val p2 = SigningKey.Secret.pair() + val p3 = SigningKey.Secret.pair() + + val ms = SignedBox(data, s1) + p2.signing + + // non tampered: + val ms1 = unpack(pack(ms)) + assertContentEquals(data, ms1.message) + assertTrue(pbk in ms1) + assertTrue(p2.aPublic in ms1) + assertTrue(p3.aPublic !in ms1) + + assertThrows { + unpack(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 { + decrypt(key, encrypt(key1, "hello".encodeToUByteArray())).decodeFromUByteArray() + } + } + +} \ No newline at end of file diff --git a/src/commonTest/kotlin/PackTest.kt b/src/commonTest/kotlin/PackTest.kt new file mode 100644 index 0000000..6a774b4 --- /dev/null +++ b/src/commonTest/kotlin/PackTest.kt @@ -0,0 +1,35 @@ +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import net.sergeych.crypto2.initCrypto +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 check(x: T?) { + assertEquals(x, unpack(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(null) + check(null) + } + @Test + fun testTimePack() = runTest { + initCrypto() + val t1 = nowToSeconds() + val t2 = t1 + 1.microseconds + assertEquals(t1, unpack(pack(t2))) + } + +} \ No newline at end of file diff --git a/src/commonTest/kotlin/ToolsTest.kt b/src/commonTest/kotlin/ToolsTest.kt new file mode 100644 index 0000000..ae19541 --- /dev/null +++ b/src/commonTest/kotlin/ToolsTest.kt @@ -0,0 +1,20 @@ +import kotlinx.coroutines.test.runTest +import net.sergeych.crypto2.createContrail +import net.sergeych.crypto2.initCrypto +import net.sergeych.crypto2.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) } + } +} \ No newline at end of file diff --git a/src/commonTest/kotlin/assertThrows.kt b/src/commonTest/kotlin/assertThrows.kt new file mode 100644 index 0000000..dc63eeb --- /dev/null +++ b/src/commonTest/kotlin/assertThrows.kt @@ -0,0 +1,13 @@ +import kotlin.test.fail + +inline fun 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") + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/net/sergeych/tools/ProtectedOp.js.kt b/src/jsMain/kotlin/net/sergeych/tools/ProtectedOp.js.kt new file mode 100644 index 0000000..acd7d7d --- /dev/null +++ b/src/jsMain/kotlin/net/sergeych/tools/ProtectedOp.js.kt @@ -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 invoke(f: () -> T): T = f() +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/net/sergeych/tools/ProtectedOp.jvm.kt b/src/jvmMain/kotlin/net/sergeych/tools/ProtectedOp.jvm.kt new file mode 100644 index 0000000..1114558 --- /dev/null +++ b/src/jvmMain/kotlin/net/sergeych/tools/ProtectedOp.jvm.kt @@ -0,0 +1,8 @@ +package net.sergeych.tools + +actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation { + private val lock = Object() + override fun invoke(f: () -> T): T { + synchronized(lock) { return f() } + } +} \ No newline at end of file diff --git a/src/nativeMain/kotlin/net/sergeych/tools/ProtectedOp.native.kt b/src/nativeMain/kotlin/net/sergeych/tools/ProtectedOp.native.kt new file mode 100644 index 0000000..c772602 --- /dev/null +++ b/src/nativeMain/kotlin/net/sergeych/tools/ProtectedOp.native.kt @@ -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 invoke(f: () -> T): T { + synchronized(lock) { + return f() + } + } +} \ No newline at end of file