forked from sergeych/crypto2
missing initial files + publishing
This commit is contained in:
parent
aaa8c436b0
commit
f429cfe418
@ -87,3 +87,21 @@ kotlin {
|
|||||||
val nativeTest by getting
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
27
src/commonMain/kotlin/net/sergeych/crypto2/InitCrypto.kt
Normal file
27
src/commonMain/kotlin/net/sergeych/crypto2/InitCrypto.kt
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
11
src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt
Normal file
11
src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt
Normal file
@ -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)
|
||||||
|
}
|
65
src/commonMain/kotlin/net/sergeych/crypto2/SignedBox.kt
Normal file
65
src/commonMain/kotlin/net/sergeych/crypto2/SignedBox.kt
Normal file
@ -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<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: 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)
|
||||||
|
}
|
||||||
|
}
|
77
src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt
Normal file
77
src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt
Normal file
@ -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")
|
7
src/commonMain/kotlin/net/sergeych/crypto2/contrail.kt
Normal file
7
src/commonMain/kotlin/net/sergeych/crypto2/contrail.kt
Normal file
@ -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
|
111
src/commonMain/kotlin/net/sergeych/crypto2/tools.kt
Normal file
111
src/commonMain/kotlin/net/sergeych/crypto2/tools.kt
Normal file
@ -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<UByte>): 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<UByte>()
|
||||||
|
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: 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()
|
||||||
|
}
|
||||||
|
}
|
10
src/commonMain/kotlin/net/sergeych/crypto2/utools.kt
Normal file
10
src/commonMain/kotlin/net/sergeych/crypto2/utools.kt
Normal file
@ -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()
|
10
src/commonMain/kotlin/net/sergeych/tools/AtomicCounter.kt
Normal file
10
src/commonMain/kotlin/net/sergeych/tools/AtomicCounter.kt
Normal file
@ -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 }
|
||||||
|
}
|
21
src/commonMain/kotlin/net/sergeych/tools/ProtectedOp.kt
Normal file
21
src/commonMain/kotlin/net/sergeych/tools/ProtectedOp.kt
Normal 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
|
22
src/commonMain/kotlin/net/sergeych/tools/flow_tools.kt
Normal file
22
src/commonMain/kotlin/net/sergeych/tools/flow_tools.kt
Normal file
@ -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 <T>Flow<T>.waitFor(predicate: (T)->Boolean) {
|
||||||
|
coroutineScope {
|
||||||
|
launch {
|
||||||
|
collect {
|
||||||
|
if( predicate(it) ) cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/commonMain/kotlin/net/sergeych/utools/collections.kt
Normal file
14
src/commonMain/kotlin/net/sergeych/utools/collections.kt
Normal file
@ -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 <T,R>Collection<T>.firstNonNull(predicate: (T)->R?): R? {
|
||||||
|
for( x in this ) predicate(x)?.let { return it }
|
||||||
|
return null
|
||||||
|
}
|
46
src/commonMain/kotlin/net/sergeych/utools/packing.kt
Normal file
46
src/commonMain/kotlin/net/sergeych/utools/packing.kt
Normal 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)
|
12
src/commonMain/kotlin/net/sergeych/utools/time.kt
Normal file
12
src/commonMain/kotlin/net/sergeych/utools/time.kt
Normal 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)
|
183
src/commonMain/kotlin/org/komputing/khash/keccak/Keccak.kt
Normal file
183
src/commonMain/kotlin/org/komputing/khash/keccak/Keccak.kt
Normal 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)
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package org.komputing.khash.keccak
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters defining the FIPS 202 standard.
|
||||||
|
*/
|
||||||
|
enum class KeccakParameter(val rateInBytes: Int,val outputLengthInBytes: Int, val d: Int) {
|
||||||
|
|
||||||
|
KECCAK_224(144, 28, 0x01),
|
||||||
|
KECCAK_256(136, 32, 0x01),
|
||||||
|
KECCAK_384(104, 48, 0x01),
|
||||||
|
KECCAK_512(72, 64, 0x01),
|
||||||
|
|
||||||
|
SHA3_224(144, 28, 0x06),
|
||||||
|
SHA3_256(136, 32, 0x06),
|
||||||
|
SHA3_384(104, 48, 0x06),
|
||||||
|
SHA3_512(72, 64, 0x06),
|
||||||
|
|
||||||
|
SHAKE128(168, 32, 0x1F),
|
||||||
|
SHAKE256(136, 64, 0x1F)
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package org.komputing.khash.keccak.extensions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns the specified int value to each element of the specified
|
||||||
|
* range in the specified array of ints. The range to be filled
|
||||||
|
* extends from index <tt>fromIndex</tt>, inclusive, to index
|
||||||
|
* <tt>toIndex</tt>, exclusive. (If <tt>fromIndex==toIndex</tt>, the
|
||||||
|
* range to be filled is empty.)
|
||||||
|
*
|
||||||
|
* @param fromIndex the index of the first element (inclusive) to be
|
||||||
|
* filled with the specified value
|
||||||
|
* @param toIndex the index of the last element (exclusive) to be
|
||||||
|
* filled with the specified value
|
||||||
|
* @param value the value to be stored in all elements of the array
|
||||||
|
* @throws IllegalArgumentException if <tt>fromIndex > toIndex</tt>
|
||||||
|
* @throws ArrayIndexOutOfBoundsException if <tt>fromIndex < 0</tt> or
|
||||||
|
* <tt>toIndex > a.length</tt>
|
||||||
|
*/
|
||||||
|
internal fun IntArray.fillWith(value: Int, fromIndex: Int = 0, toIndex: Int = this.size) {
|
||||||
|
if (fromIndex > toIndex) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"fromIndex($fromIndex) > toIndex($toIndex)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromIndex < 0) {
|
||||||
|
throw ArrayIndexOutOfBoundsException(fromIndex)
|
||||||
|
}
|
||||||
|
if (toIndex > this.size) {
|
||||||
|
throw ArrayIndexOutOfBoundsException(toIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in fromIndex until toIndex)
|
||||||
|
this[i] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new [ArrayIndexOutOfBoundsException]
|
||||||
|
* class with an argument indicating the illegal index.
|
||||||
|
* @param index the illegal index.
|
||||||
|
*/
|
||||||
|
internal class ArrayIndexOutOfBoundsException(index: Int) : Throwable("Array index out of range: $index")
|
@ -0,0 +1,19 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
package org.komputing.khash.keccak.extensions
|
||||||
|
|
||||||
|
import org.komputing.khash.keccak.Keccak
|
||||||
|
import org.komputing.khash.keccak.KeccakParameter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the proper Keccak digest of [this] byte array based on the given [parameter]
|
||||||
|
*/
|
||||||
|
fun ByteArray.digestKeccak(parameter: KeccakParameter): ByteArray {
|
||||||
|
return Keccak.digest(this, parameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the proper Keccak digest of [this] string based on the given [parameter]
|
||||||
|
*/
|
||||||
|
fun String.digestKeccak(parameter: KeccakParameter): ByteArray {
|
||||||
|
return Keccak.digest(encodeToByteArray(), parameter)
|
||||||
|
}
|
58
src/commonTest/kotlin/KeysTest.kt
Normal file
58
src/commonTest/kotlin/KeysTest.kt
Normal 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.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<SignedBox>(pack(ms))
|
||||||
|
assertContentEquals(data, ms1.message)
|
||||||
|
assertTrue(pbk in ms1)
|
||||||
|
assertTrue(p2.aPublic in ms1)
|
||||||
|
assertTrue(p3.aPublic !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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
35
src/commonTest/kotlin/PackTest.kt
Normal file
35
src/commonTest/kotlin/PackTest.kt
Normal file
@ -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 <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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
src/commonTest/kotlin/ToolsTest.kt
Normal file
20
src/commonTest/kotlin/ToolsTest.kt
Normal file
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
13
src/commonTest/kotlin/assertThrows.kt
Normal file
13
src/commonTest/kotlin/assertThrows.kt
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
6
src/jsMain/kotlin/net/sergeych/tools/ProtectedOp.js.kt
Normal file
6
src/jsMain/kotlin/net/sergeych/tools/ProtectedOp.js.kt
Normal 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()
|
||||||
|
}
|
8
src/jvmMain/kotlin/net/sergeych/tools/ProtectedOp.jvm.kt
Normal file
8
src/jvmMain/kotlin/net/sergeych/tools/ProtectedOp.jvm.kt
Normal 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() }
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user