111 lines
3.7 KiB
Kotlin
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()
|
|
}
|
|
}
|