forked from sergeych/crypto2
missing initial files + publishing
This commit is contained in:
1 parent
aaa8c436b0
commit
f429cfe418
25 files changed
+870
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.sergeych.tools
|
||||
|
||||
/**
|
||||
* Multiplatform interface to perform a regular (not suspend) operation
|
||||
* protected by a platform mutex (where necessary). Get real implementation
|
||||
* with [ProtectedOp]
|
||||
*/
|
||||
interface ProtectedOpImplementation {
|
||||
/**
|
||||
* Call [f] iin mutually exclusive mode, it means that only one invocation
|
||||
* can be active at a time, all the rest are waiting until the current operation
|
||||
* will finish.
|
||||
*/
|
||||
operator fun <T>invoke(f: ()->T): T
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the platform-depended implementation of a mutex-protected operation.
|
||||
*/
|
||||
expect fun ProtectedOp(): ProtectedOpImplementation
|
||||
@@ -0,0 +1,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.sergeych.utools
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.serializer
|
||||
import net.sergeych.bintools.toDataSource
|
||||
import net.sergeych.bipack.BipackDecoder
|
||||
import net.sergeych.bipack.BipackEncoder
|
||||
|
||||
/**
|
||||
* Effectively pack anyk nullable object. The result could be effectively packed
|
||||
* in turn as a part of a more complex structure.
|
||||
*
|
||||
* To avoid packing non-null mark,
|
||||
* we use a zero-size array, which, if in turn encoded, packs into a single
|
||||
* zero byte. Thus, we avoid extra byte spending for unnecessary null
|
||||
* check.
|
||||
*/
|
||||
inline fun <reified T> pack(element: T?): UByteArray = pack(serializer<T>(), element)
|
||||
|
||||
/**
|
||||
* Unpack nullable data packed with [pack]
|
||||
*/
|
||||
inline fun <reified T: Any?> unpack(encoded: UByteArray): T =
|
||||
unpack(serializer<T>(), encoded)
|
||||
|
||||
/**
|
||||
* Effectively pack anyk nullable object. The result could be effectively packed
|
||||
* in turn as a part of a more complex structure.
|
||||
*
|
||||
* To avoid packing non-null mark,
|
||||
* we use a zero-size array, which, if in turn encoded, packs into a single
|
||||
* zero byte. Thus, we avoid extra byte spending for unnecessary null
|
||||
* check.
|
||||
*/
|
||||
fun <T>pack(serializer: KSerializer<T>, element: T?): UByteArray =
|
||||
if (element == null) ubyteArrayOf()
|
||||
else BipackEncoder.encode(serializer,element).toUByteArray()
|
||||
|
||||
|
||||
/**
|
||||
* Unpack nullable data packed with [pack]
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T: Any?> unpack(serializer: KSerializer<T>, encoded: UByteArray): T =
|
||||
if (encoded.isEmpty()) null as T
|
||||
else BipackDecoder.decode(encoded.toByteArray().toDataSource(),serializer)
|
||||
@@ -0,0 +1,12 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.sergeych.utools
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
fun now(): Instant = Clock.System.now()
|
||||
fun nowToSeconds(): Instant = Clock.System.now().truncateToSeconds()
|
||||
|
||||
fun Instant.truncateToSeconds(): Instant =
|
||||
Instant.fromEpochSeconds(toEpochMilliseconds()/1000)
|
||||
Reference in new issue
Block a user