libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

TalerCommon.kt (27697B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024, 2025, 2026 Taler Systems S.A.
      4  *
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9  *
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14  *
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 package tech.libeufin.common
     21 
     22 import io.ktor.http.*
     23 import io.ktor.server.plugins.*
     24 import kotlinx.serialization.*
     25 import kotlinx.serialization.descriptors.*
     26 import kotlinx.serialization.encoding.*
     27 import kotlinx.serialization.json.*
     28 import java.time.Instant
     29 import java.time.Duration
     30 import java.time.temporal.ChronoUnit
     31 import java.util.concurrent.TimeUnit
     32 import org.bouncycastle.math.ec.rfc8032.Ed25519
     33 import io.github.smiley4.schemakenerator.core.annotations.Description
     34 
     35 sealed class CommonError(msg: String) : Exception(msg) {
     36     class AmountFormat(msg: String) : CommonError(msg)
     37     class AmountNumberTooBig(msg: String) : CommonError(msg)
     38     class Payto(msg: String) : CommonError(msg)
     39 }
     40 
     41 /**
     42  * Internal representation of relative times.  The
     43  * "forever" case is represented with Long.MAX_VALUE.
     44  */
     45 @Description("Relative time duration, serialized as microseconds or 'forever'")
     46 @JvmInline
     47 @Serializable(with = RelativeTime.Serializer::class)
     48 value class RelativeTime(val duration: Duration) {
     49     internal object Serializer : KSerializer<RelativeTime> {
     50         override val descriptor: SerialDescriptor =
     51             buildClassSerialDescriptor("RelativeTime") {
     52                 element<JsonElement>("d_us")
     53             }
     54 
     55         override fun serialize(encoder: Encoder, value: RelativeTime) {
     56             val composite = encoder.beginStructure(descriptor)
     57             if (value.duration == ChronoUnit.FOREVER.duration) {
     58                 composite.encodeStringElement(descriptor, 0, "forever")
     59             } else {
     60                 composite.encodeLongElement(descriptor, 0, TimeUnit.MICROSECONDS.convert(value.duration))
     61             }
     62             composite.endStructure(descriptor)
     63         }
     64 
     65         override fun deserialize(decoder: Decoder): RelativeTime {
     66             val dec = decoder.beginStructure(descriptor)
     67             val jsonInput = dec as? JsonDecoder ?: error("Can be deserialized only by JSON")
     68             lateinit var maybeDUs: JsonPrimitive
     69             loop@ while (true) {
     70                 when (val index = dec.decodeElementIndex(descriptor)) {
     71                     0 -> maybeDUs = jsonInput.decodeJsonElement().jsonPrimitive
     72                     CompositeDecoder.DECODE_DONE -> break@loop
     73                     else -> throw SerializationException("Unexpected index: $index")
     74                 }
     75             }
     76             dec.endStructure(descriptor)
     77             if (maybeDUs.isString) {
     78                 if (maybeDUs.content != "forever") throw badRequest("Only 'forever' allowed for d_us as string, but '${maybeDUs.content}' was found")
     79                 return RelativeTime(ChronoUnit.FOREVER.duration)
     80             }
     81             val dUs: Long = maybeDUs.longOrNull
     82                 ?: throw badRequest("Could not convert d_us: '${maybeDUs.content}' to a number")
     83             when {
     84                 dUs < 0 -> throw badRequest("Negative duration specified.")
     85                 dUs > MAX_SAFE_INTEGER -> throw badRequest("d_us value $dUs exceed cap (2^53-1)")
     86                 else -> return RelativeTime(Duration.of(dUs, ChronoUnit.MICROS))
     87             }
     88         }
     89     }
     90 
     91     companion object {
     92         const val MAX_SAFE_INTEGER = 9007199254740991L // 2^53 - 1
     93     }
     94 }
     95 
     96 /** Timestamp containing the number of seconds since epoch */
     97 @Description("Timestamp as seconds since Unix epoch, or 'never'")
     98 @JvmInline
     99 @Serializable(with = TalerTimestamp.Serializer::class)
    100 value class TalerTimestamp constructor(val instant: Instant) {
    101     internal object Serializer : KSerializer<TalerTimestamp> {
    102         override val descriptor: SerialDescriptor =
    103             buildClassSerialDescriptor("Timestamp") {
    104                 element<JsonElement>("t_s")
    105             }
    106 
    107         override fun serialize(encoder: Encoder, value: TalerTimestamp) {
    108             val composite = encoder.beginStructure(descriptor)
    109             if (value.instant == Instant.MAX) {
    110                 composite.encodeStringElement(descriptor, 0, "never")
    111             } else {
    112                 composite.encodeLongElement(descriptor, 0, value.instant.epochSecond)
    113             }
    114             composite.endStructure(descriptor)
    115         }
    116 
    117         override fun deserialize(decoder: Decoder): TalerTimestamp {
    118             val dec = decoder.beginStructure(descriptor)
    119             val jsonInput = dec as? JsonDecoder ?: error("Can be deserialized only by JSON")
    120             lateinit var maybeTs: JsonPrimitive
    121             loop@ while (true) {
    122                 when (val index = dec.decodeElementIndex(descriptor)) {
    123                     0 -> maybeTs = jsonInput.decodeJsonElement().jsonPrimitive
    124                     CompositeDecoder.DECODE_DONE -> break@loop
    125                     else -> throw SerializationException("Unexpected index: $index")
    126                 }
    127             }
    128             dec.endStructure(descriptor)
    129             if (maybeTs.isString) {
    130                 if (maybeTs.content != "never") throw badRequest("Only 'never' allowed for t_s as string, but '${maybeTs.content}' was found")
    131                 return TalerTimestamp(Instant.MAX)
    132             }
    133             val ts: Long = maybeTs.longOrNull
    134                 ?: throw badRequest("Could not convert t_s '${maybeTs.content}' to a number")
    135             when {
    136                 ts < 0 -> throw badRequest("Negative timestamp not allowed")
    137                 ts > Instant.MAX.epochSecond -> throw badRequest("Timestamp $ts too big to be represented in Kotlin")
    138                 else -> return TalerTimestamp(Instant.ofEpochSecond(ts))
    139             }
    140         }
    141     }
    142 
    143     companion object {
    144         fun never(): TalerTimestamp = TalerTimestamp(Instant.MAX)
    145     }
    146 }
    147 
    148 @Description("Base URL string ending with a trailing slash")
    149 @JvmInline
    150 @Serializable(with = BaseURL.Serializer::class)
    151 value class BaseURL private constructor(val url: Url) {
    152     companion object {
    153         fun parse(raw: String): BaseURL {
    154             val url = URLBuilder(raw)
    155             if (url.protocolOrNull == null) {
    156                 throw badRequest("missing protocol in baseURL got '${url}'")
    157             } else if (url.protocol.name !in setOf("http", "https")) {
    158                 throw badRequest("only 'http' and 'https' are accepted for baseURL got '${url.protocol.name}'")
    159             } else if (url.host.isEmpty()) {
    160                 throw badRequest("missing host in baseURL got '${url}'")
    161             } else if (!url.parameters.isEmpty()) {
    162                 throw badRequest("require no query in baseURL got '${url.encodedParameters}'")
    163             } else if (url.fragment.isNotEmpty()) {
    164                 throw badRequest("require no fragments in baseURL got '${url.fragment}'")
    165             } else if (!url.encodedPath.endsWith('/')) {
    166                 throw badRequest("baseURL path must end with / got '${url.encodedPath}'")
    167             }
    168             return BaseURL(url.build())
    169         }
    170     }
    171 
    172     override fun toString(): String = url.toString()
    173 
    174     internal object Serializer : KSerializer<BaseURL> {
    175         override val descriptor: SerialDescriptor =
    176             PrimitiveSerialDescriptor("BaseURL", PrimitiveKind.STRING)
    177 
    178         override fun serialize(encoder: Encoder, value: BaseURL) {
    179             encoder.encodeString(value.url.toString())
    180         }
    181 
    182         override fun deserialize(decoder: Decoder): BaseURL {
    183             return BaseURL.parse(decoder.decodeString())
    184         }
    185     }
    186 }
    187 
    188 @Serializable(with = DecimalNumber.Serializer::class)
    189 class DecimalNumber {
    190     val value: Long
    191     val frac: Int
    192 
    193     constructor(value: Long, frac: Int) {
    194         this.value = value
    195         this.frac = frac
    196     }
    197 
    198     constructor(encoded: String) {
    199         val match = PATTERN.matchEntire(encoded) ?: throw badRequest("Invalid decimal number format")
    200         val (value, frac) = match.destructured
    201         this.value = value.toLongOrNull() ?: throw badRequest("Invalid value")
    202         if (this.value > TalerAmount.MAX_VALUE)
    203             throw badRequest("Value specified in decimal number is too large")
    204         this.frac = if (frac.isEmpty()) {
    205             0
    206         } else {
    207             var tmp = frac.toIntOrNull() ?: throw badRequest("Invalid fractional value")
    208             if (tmp > TalerAmount.FRACTION_BASE)
    209                 throw badRequest("Fractional value specified in decimal number is too large")
    210             repeat(8 - frac.length) {
    211                 tmp *= 10
    212             }
    213             tmp
    214         }
    215     }
    216 
    217     fun isZero(): Boolean = value == 0L && frac == 0
    218 
    219     override fun equals(other: Any?): Boolean {
    220         return other is DecimalNumber &&
    221                 other.value == this.value &&
    222                 other.frac == this.frac
    223     }
    224 
    225     override fun toString(): String {
    226         return if (frac == 0) {
    227             "$value"
    228         } else {
    229             "$value.${frac.toString().padStart(8, '0')}"
    230                 .dropLastWhile { it == '0' } // Trim useless fractional trailing 0
    231         }
    232     }
    233 
    234     internal object Serializer : KSerializer<DecimalNumber> {
    235         override val descriptor: SerialDescriptor =
    236             PrimitiveSerialDescriptor("DecimalNumber", PrimitiveKind.STRING)
    237 
    238         override fun serialize(encoder: Encoder, value: DecimalNumber) {
    239             encoder.encodeString(value.toString())
    240         }
    241 
    242         override fun deserialize(decoder: Decoder): DecimalNumber {
    243             return DecimalNumber(decoder.decodeString())
    244         }
    245     }
    246 
    247     companion object {
    248         val ZERO = DecimalNumber(0, 0)
    249         private val PATTERN = Regex("([0-9]+)(?:\\.([0-9]{1,8}))?")
    250     }
    251 }
    252 
    253 @Serializable(with = TalerAmount.Serializer::class)
    254 class TalerAmount : Comparable<TalerAmount> {
    255     val value: Long
    256     val frac: Int
    257     val currency: String
    258 
    259     constructor(value: Long, frac: Int, currency: String) {
    260         this.value = value
    261         this.frac = frac
    262         this.currency = currency
    263     }
    264 
    265     constructor(encoded: String) {
    266         val match = PATTERN.matchEntire(encoded) ?: throw CommonError.AmountFormat("Invalid amount format")
    267         val (currency, value, frac) = match.destructured
    268         this.currency = currency
    269         this.value = value.toLongOrNull() ?: throw CommonError.AmountFormat("Invalid value")
    270         if (this.value > MAX_VALUE)
    271             throw CommonError.AmountNumberTooBig("Value specified in amount is too large")
    272         this.frac = if (frac.isEmpty()) {
    273             0
    274         } else {
    275             var tmp = frac.toIntOrNull() ?: throw CommonError.AmountFormat("Invalid fractional value")
    276             if (tmp > FRACTION_BASE)
    277                 throw CommonError.AmountFormat("Fractional value specified in amount is too large")
    278             repeat(8 - frac.length) {
    279                 tmp *= 10
    280             }
    281 
    282             tmp
    283         }
    284     }
    285 
    286     fun number(): DecimalNumber = DecimalNumber(value, frac)
    287 
    288     /* Check if zero */
    289     fun isZero(): Boolean = value == 0L && frac == 0
    290 
    291     fun notZeroOrNull(): TalerAmount? = if (isZero()) null else this
    292 
    293     /* Check is amount has fractional amount < 0.01 */
    294     fun isSubCent(): Boolean = (frac % CENT_FRACTION) > 0
    295 
    296     override fun equals(other: Any?): Boolean {
    297         return other is TalerAmount &&
    298                 other.value == this.value &&
    299                 other.frac == this.frac &&
    300                 other.currency == this.currency
    301     }
    302 
    303     override fun toString(): String {
    304         return if (frac == 0) {
    305             "$currency:$value"
    306         } else {
    307             "$currency:$value.${frac.toString().padStart(8, '0')}"
    308                 .dropLastWhile { it == '0' } // Trim useless fractional trailing 0
    309         }
    310     }
    311 
    312     fun normalize(): TalerAmount {
    313         val value = Math.addExact(this.value, (this.frac / FRACTION_BASE).toLong())
    314         val frac = this.frac % FRACTION_BASE
    315         if (value > MAX_VALUE) throw ArithmeticException("amount value overflowed")
    316         return TalerAmount(value, frac, currency)
    317     }
    318 
    319     override operator fun compareTo(other: TalerAmount) = compareValuesBy(this, other, { it.value }, { it.frac })
    320 
    321     operator fun plus(increment: TalerAmount): TalerAmount {
    322         require(this.currency == increment.currency) { "currency mismatch ${this.currency} != ${increment.currency}" }
    323         val value = Math.addExact(this.value, increment.value)
    324         val frac = Math.addExact(this.frac, increment.frac)
    325         return TalerAmount(value, frac, currency).normalize()
    326     }
    327 
    328     operator fun minus(decrement: TalerAmount): TalerAmount {
    329         require(this.currency == decrement.currency) { "currency mismatch ${this.currency} != ${decrement.currency}" }
    330         var frac = this.frac
    331         var value = this.value
    332         if (frac < decrement.frac) {
    333             if (value <= 0) {
    334                 throw ArithmeticException("negative result")
    335             }
    336             frac += FRACTION_BASE
    337             value -= 1
    338         }
    339         if (value < decrement.value) {
    340             throw ArithmeticException("negative result")
    341         }
    342         return TalerAmount(value - decrement.value, frac - decrement.frac, currency).normalize()
    343     }
    344 
    345     internal object Serializer : KSerializer<TalerAmount> {
    346         override val descriptor: SerialDescriptor =
    347             PrimitiveSerialDescriptor("TalerAmount", PrimitiveKind.STRING)
    348 
    349         override fun serialize(encoder: Encoder, value: TalerAmount) {
    350             encoder.encodeString(value.toString())
    351         }
    352 
    353         override fun deserialize(decoder: Decoder): TalerAmount =
    354             TalerAmount(decoder.decodeString())
    355 
    356     }
    357 
    358     companion object {
    359         const val FRACTION_BASE = 100000000
    360         const val CENT_FRACTION = 1000000
    361         const val MAX_VALUE = 4503599627370496L // 2^52
    362         private val PATTERN = Regex("([A-Z]{1,11}):([0-9]+)(?:\\.([0-9]{1,8}))?")
    363 
    364         fun zero(currency: String) = TalerAmount(0, 0, currency)
    365         fun max(currency: String) = TalerAmount(MAX_VALUE, FRACTION_BASE - 1, currency)
    366     }
    367 }
    368 
    369 @Serializable(with = Payto.Serializer::class)
    370 sealed class Payto {
    371     abstract val parsed: Url
    372     abstract val canonical: String
    373     abstract val amount: TalerAmount?
    374     abstract val message: String?
    375     abstract val receiverName: String?
    376 
    377     /** Transform a payto URI to its bank form, using [name] as the receiver-name and the bank [ctx] */
    378     fun bank(name: String?, ctx: BankPaytoCtx): String = when (this) {
    379         is IbanPayto -> IbanPayto.build(iban.toString(), ctx.bic, name)
    380         is XTalerBankPayto -> {
    381             val name = if (name != null) "?receiver-name=${name.encodeURLParameter()}" else ""
    382             "payto://x-taler-bank/${ctx.hostname}/$username$name"
    383         }
    384     }
    385 
    386     fun expectIbanFull(): IbanPayto {
    387         val payto = expectIban()
    388         if (payto.receiverName == null) {
    389             throw CommonError.Payto("expected a full IBAN payto got no receiver-name")
    390         }
    391         return payto
    392     }
    393 
    394     fun expectIban(): IbanPayto {
    395         return when (this) {
    396             is IbanPayto -> this
    397             else -> throw CommonError.Payto("expected an IBAN payto URI got '${parsed.host}'")
    398         }
    399     }
    400 
    401     fun expectXTalerBank(): XTalerBankPayto {
    402         return when (this) {
    403             is XTalerBankPayto -> this
    404             else -> throw CommonError.Payto("expected a x-taler-bank payto URI got '${parsed.host}'")
    405         }
    406     }
    407 
    408     override fun equals(other: Any?): Boolean {
    409         if (this === other) return true
    410         if (other !is Payto) return false
    411         return this.parsed == other.parsed
    412     }
    413 
    414     internal object Serializer : KSerializer<Payto> {
    415         override val descriptor: SerialDescriptor =
    416             PrimitiveSerialDescriptor("Payto", PrimitiveKind.STRING)
    417 
    418         override fun serialize(encoder: Encoder, value: Payto) {
    419             encoder.encodeString(value.toString())
    420         }
    421 
    422         override fun deserialize(decoder: Decoder): Payto {
    423             return parse(decoder.decodeString())
    424         }
    425     }
    426 
    427     companion object {
    428         private val HEX_PATTERN: Regex = Regex("%(?![0-9a-fA-F]{2})")
    429         fun parse(input: String): Payto {
    430             val raw = input.replace(HEX_PATTERN, "%25")
    431             val parsed = try {
    432                 Url(raw)
    433             } catch (e: Exception) {
    434                 throw CommonError.Payto("expected a valid URI")
    435             }
    436             if (parsed.protocol.name != "payto") throw CommonError.Payto("expect a payto URI got '${parsed.protocol.name}'")
    437 
    438             val amount = parsed.parameters["amount"]?.run { TalerAmount(this) }
    439             val message = parsed.parameters["message"]
    440             val receiverName = parsed.parameters["receiver-name"]
    441 
    442             return when (parsed.host) {
    443                 "iban" -> {
    444                     val segments = parsed.segments
    445                     val (bic, rawIban) = when (segments.size) {
    446                         1 -> Pair(null, segments[0])
    447                         2 -> Pair(segments[0], segments[1])
    448                         else -> throw CommonError.Payto("too many path segments for an IBAN payto URI")
    449                     }
    450                     val iban = IBAN.parse(rawIban)
    451                     IbanPayto(
    452                         parsed,
    453                         "payto://iban/$iban",
    454                         amount,
    455                         message,
    456                         receiverName,
    457                         parsed.parameters["ch-qrr"],
    458                         bic,
    459                         iban,
    460                     )
    461                 }
    462 
    463                 "x-taler-bank" -> {
    464                     val segments = parsed.segments
    465                     if (segments.size != 2)
    466                         throw CommonError.Payto("bad number of path segments for a x-taler-bank payto URI")
    467                     val username = segments[1]
    468                     XTalerBankPayto(
    469                         parsed,
    470                         "payto://x-taler-bank/localhost/$username",
    471                         amount,
    472                         message,
    473                         receiverName,
    474                         username
    475                     )
    476                 }
    477 
    478                 else -> throw CommonError.Payto("unsupported payto URI kind '${parsed.host}'")
    479             }
    480         }
    481     }
    482 }
    483 
    484 @Serializable(with = IbanPayto.Serializer::class)
    485 class IbanPayto internal constructor(
    486     override val parsed: Url,
    487     override val canonical: String,
    488     override val amount: TalerAmount?,
    489     override val message: String?,
    490     override val receiverName: String?,
    491     val chQrr: String?,
    492     val bic: String?,
    493     val iban: IBAN
    494 ) : Payto() {
    495     override fun toString(): String = parsed.toString()
    496 
    497     /** Format an IbanPayto in a more human readable way */
    498     fun fmt(): String = buildString {
    499         append('(')
    500         append(iban)
    501         if (bic != null) {
    502             append(' ')
    503             append(bic)
    504         }
    505         if (receiverName != null) {
    506             append(' ')
    507             append(receiverName)
    508         }
    509         append(')')
    510     }
    511 
    512     /** Transform an IBAN payto URI to its simple form without any query */
    513     fun simple(): String = build(iban.toString(), bic, null)
    514 
    515     /** Transform an IBAN payto URI to its full form, using [name] as its receiver-name */
    516     fun full(name: String): String = build(iban.toString(), bic, name)
    517 
    518     internal object Serializer : KSerializer<IbanPayto> {
    519         override val descriptor: SerialDescriptor =
    520             PrimitiveSerialDescriptor("IbanPayto", PrimitiveKind.STRING)
    521 
    522         override fun serialize(encoder: Encoder, value: IbanPayto) {
    523             encoder.encodeString(value.toString())
    524         }
    525 
    526         override fun deserialize(decoder: Decoder): IbanPayto {
    527             return parse(decoder.decodeString()).expectIban()
    528         }
    529     }
    530 
    531     companion object {
    532         fun build(iban: String, bic: String?, name: String?): String {
    533             val bic = if (bic != null) "$bic/" else ""
    534             val name = if (name != null) "?receiver-name=${name.encodeURLParameter()}" else ""
    535             return "payto://iban/$bic$iban$name"
    536         }
    537 
    538         fun rand(name: String? = null, country: Country = Country.DE): IbanPayto = parse(
    539             "payto://iban/SANDBOXX/${IBAN.rand(country)}${
    540                 if (name != null) {
    541                     "?receiver-name=${name.encodeURLParameter()}"
    542                 } else {
    543                     ""
    544                 }
    545             }"
    546         ).expectIban()
    547     }
    548 }
    549 
    550 class XTalerBankPayto internal constructor(
    551     override val parsed: Url,
    552     override val canonical: String,
    553     override val amount: TalerAmount?,
    554     override val message: String?,
    555     override val receiverName: String?,
    556     val username: String
    557 ) : Payto() {
    558     override fun toString(): String = parsed.toString()
    559 
    560     companion object {
    561         fun forUsername(username: String): XTalerBankPayto {
    562             return parse("payto://x-taler-bank/hostname/$username").expectXTalerBank()
    563         }
    564     }
    565 }
    566 
    567 /** Context specific data necessary to create a bank payto URI from a canonical payto URI */
    568 data class BankPaytoCtx(
    569     val bic: String?,
    570     val hostname: String
    571 )
    572 
    573 
    574 /** 16-byte Crockford's Base32 encoded data */
    575 @Serializable(with = Base32Crockford16B.Serializer::class)
    576 class Base32Crockford16B {
    577     private var encoded: String? = null
    578     val raw: ByteArray
    579 
    580     constructor(encoded: String) {
    581         val decoded = try {
    582             Base32Crockford.decode(encoded)
    583         } catch (e: IllegalArgumentException) {
    584             null
    585         }
    586         require(decoded != null && decoded.size == 16) {
    587             "expected 16 bytes encoded in Crockford's base32"
    588         }
    589         this.raw = decoded
    590         this.encoded = encoded
    591     }
    592 
    593     constructor(raw: ByteArray) {
    594         require(raw.size == 16) {
    595             "encoded data should be 16 bytes long"
    596         }
    597         this.raw = raw
    598     }
    599 
    600     fun encoded(): String {
    601         val tmp = encoded ?: Base32Crockford.encode(raw)
    602         encoded = tmp
    603         return tmp
    604     }
    605 
    606     override fun toString(): String {
    607         return encoded()
    608     }
    609 
    610     override fun equals(other: Any?) = (other is Base32Crockford16B) && raw.contentEquals(other.raw)
    611 
    612     internal object Serializer : KSerializer<Base32Crockford16B> {
    613         override val descriptor: SerialDescriptor =
    614             PrimitiveSerialDescriptor("Base32Crockford16B", PrimitiveKind.STRING)
    615 
    616         override fun serialize(encoder: Encoder, value: Base32Crockford16B) {
    617             encoder.encodeString(value.encoded())
    618         }
    619 
    620         override fun deserialize(decoder: Decoder): Base32Crockford16B {
    621             return Base32Crockford16B(decoder.decodeString())
    622         }
    623     }
    624 
    625     companion object {
    626         fun rand(): Base32Crockford16B = Base32Crockford16B(ByteArray(16).rand())
    627         fun secureRand(): Base32Crockford16B = Base32Crockford16B(ByteArray(16).secureRand())
    628     }
    629 }
    630 
    631 /** 32-byte Crockford's Base32 encoded data */
    632 @Description("32-byte Crockford Base32 encoded data")
    633 @Serializable(with = Base32Crockford32B.Serializer::class)
    634 class Base32Crockford32B {
    635     private var encoded: String? = null
    636     val raw: ByteArray
    637 
    638     constructor(encoded: String) {
    639         val decoded = try {
    640             Base32Crockford.decode(encoded)
    641         } catch (e: IllegalArgumentException) {
    642             null
    643         }
    644         require(decoded != null && decoded.size == 32) {
    645             "expected 32 bytes encoded in Crockford's base32"
    646         }
    647         this.raw = decoded
    648         this.encoded = encoded
    649     }
    650 
    651     constructor(raw: ByteArray) {
    652         require(raw.size == 32) {
    653             "encoded data should be 32 bytes long"
    654         }
    655         this.raw = raw
    656     }
    657 
    658     fun encoded(): String {
    659         val tmp = encoded ?: Base32Crockford.encode(raw)
    660         encoded = tmp
    661         return tmp
    662     }
    663 
    664     override fun toString(): String {
    665         return encoded()
    666     }
    667 
    668     override fun equals(other: Any?) = (other is Base32Crockford32B) && raw.contentEquals(other.raw)
    669 
    670     internal object Serializer : KSerializer<Base32Crockford32B> {
    671         override val descriptor: SerialDescriptor =
    672             PrimitiveSerialDescriptor("Base32Crockford32B", PrimitiveKind.STRING)
    673 
    674         override fun serialize(encoder: Encoder, value: Base32Crockford32B) {
    675             encoder.encodeString(value.encoded())
    676         }
    677 
    678         override fun deserialize(decoder: Decoder): Base32Crockford32B {
    679             return Base32Crockford32B(decoder.decodeString())
    680         }
    681     }
    682 
    683     companion object {
    684         fun rand(): Base32Crockford32B = Base32Crockford32B(ByteArray(32).rand())
    685         fun secureRand(): Base32Crockford32B = Base32Crockford32B(ByteArray(32).secureRand())
    686         fun randEdsaKey(): EddsaPublicKey = randEdsaKeyPair().second
    687         fun randEdsaKeyPair(): Pair<ByteArray, EddsaPublicKey> {
    688             val secretKey = ByteArray(32)
    689             Ed25519.generatePrivateKey(SECURE_RNG.get(), secretKey)
    690             val publicKey = ByteArray(32)
    691             Ed25519.generatePublicKey(secretKey, 0, publicKey, 0)
    692             return Pair(secretKey, Base32Crockford32B(publicKey))
    693         }
    694     }
    695 }
    696 
    697 /** 64-byte Crockford's Base32 encoded data */
    698 @Description("64-byte Crockford Base32 encoded data")
    699 @Serializable(with = Base32Crockford64B.Serializer::class)
    700 class Base32Crockford64B {
    701     private var encoded: String? = null
    702     val raw: ByteArray
    703 
    704     constructor(encoded: String) {
    705         val decoded = try {
    706             Base32Crockford.decode(encoded)
    707         } catch (e: IllegalArgumentException) {
    708             null
    709         }
    710 
    711         require(decoded != null && decoded.size == 64) {
    712             "expected 64 bytes encoded in Crockford's base32"
    713         }
    714         this.raw = decoded
    715         this.encoded = encoded
    716     }
    717 
    718     constructor(raw: ByteArray) {
    719         require(raw.size == 64) {
    720             "encoded data should be 64 bytes long"
    721         }
    722         this.raw = raw
    723     }
    724 
    725     fun encoded(): String {
    726         val tmp = encoded ?: Base32Crockford.encode(raw)
    727         encoded = tmp
    728         return tmp
    729     }
    730 
    731     override fun toString(): String {
    732         return encoded()
    733     }
    734 
    735     override fun equals(other: Any?) = (other is Base32Crockford64B) && raw.contentEquals(other.raw)
    736 
    737     internal object Serializer : KSerializer<Base32Crockford64B> {
    738         override val descriptor: SerialDescriptor =
    739             PrimitiveSerialDescriptor("Base32Crockford64B", PrimitiveKind.STRING)
    740 
    741         override fun serialize(encoder: Encoder, value: Base32Crockford64B) {
    742             encoder.encodeString(value.encoded())
    743         }
    744 
    745         override fun deserialize(decoder: Decoder): Base32Crockford64B {
    746             return Base32Crockford64B(decoder.decodeString())
    747         }
    748     }
    749 
    750     companion object {
    751         fun rand(): Base32Crockford64B = Base32Crockford64B(ByteArray(64).rand())
    752     }
    753 }
    754 
    755 /** 32-byte hash code */
    756 typealias ShortHashCode = Base32Crockford32B
    757 /** 64-byte hash code */
    758 typealias HashCode = Base32Crockford64B
    759 
    760 typealias EddsaSignature = Base32Crockford64B
    761 /**
    762  * EdDSA and ECDHE public keys always point on Curve25519
    763  * and represented  using the standard 256 bits Ed25519 compact format,
    764  * converted to Crockford Base32.
    765  */
    766 typealias EddsaPublicKey = Base32Crockford32B