111 lines
3.7 KiB
Kotlin

@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.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 shr 7) or (b and 0x7fu)
if( (b and 0x80u) != 0u ) break
if( ++cnt > 4 ) throw IllegalArgumentException("overflow while decoding varuint")
}
return result
}
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()
}
}