libeufin

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

TalerCommon.kt (27595B)


      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                         bic,
    458                         iban
    459                     )
    460                 }
    461 
    462                 "x-taler-bank" -> {
    463                     val segments = parsed.segments
    464                     if (segments.size != 2)
    465                         throw CommonError.Payto("bad number of path segments for a x-taler-bank payto URI")
    466                     val username = segments[1]
    467                     XTalerBankPayto(
    468                         parsed,
    469                         "payto://x-taler-bank/localhost/$username",
    470                         amount,
    471                         message,
    472                         receiverName,
    473                         username
    474                     )
    475                 }
    476 
    477                 else -> throw CommonError.Payto("unsupported payto URI kind '${parsed.host}'")
    478             }
    479         }
    480     }
    481 }
    482 
    483 @Serializable(with = IbanPayto.Serializer::class)
    484 class IbanPayto internal constructor(
    485     override val parsed: Url,
    486     override val canonical: String,
    487     override val amount: TalerAmount?,
    488     override val message: String?,
    489     override val receiverName: String?,
    490     val bic: String?,
    491     val iban: IBAN
    492 ) : Payto() {
    493     override fun toString(): String = parsed.toString()
    494 
    495     /** Format an IbanPayto in a more human readable way */
    496     fun fmt(): String = buildString {
    497         append('(')
    498         append(iban)
    499         if (bic != null) {
    500             append(' ')
    501             append(bic)
    502         }
    503         if (receiverName != null) {
    504             append(' ')
    505             append(receiverName)
    506         }
    507         append(')')
    508     }
    509 
    510     /** Transform an IBAN payto URI to its simple form without any query */
    511     fun simple(): String = build(iban.toString(), bic, null)
    512 
    513     /** Transform an IBAN payto URI to its full form, using [name] as its receiver-name */
    514     fun full(name: String): String = build(iban.toString(), bic, name)
    515 
    516     internal object Serializer : KSerializer<IbanPayto> {
    517         override val descriptor: SerialDescriptor =
    518             PrimitiveSerialDescriptor("IbanPayto", PrimitiveKind.STRING)
    519 
    520         override fun serialize(encoder: Encoder, value: IbanPayto) {
    521             encoder.encodeString(value.toString())
    522         }
    523 
    524         override fun deserialize(decoder: Decoder): IbanPayto {
    525             return parse(decoder.decodeString()).expectIban()
    526         }
    527     }
    528 
    529     companion object {
    530         fun build(iban: String, bic: String?, name: String?): String {
    531             val bic = if (bic != null) "$bic/" else ""
    532             val name = if (name != null) "?receiver-name=${name.encodeURLParameter()}" else ""
    533             return "payto://iban/$bic$iban$name"
    534         }
    535 
    536         fun rand(name: String? = null): IbanPayto = parse(
    537             "payto://iban/SANDBOXX/${IBAN.rand(Country.DE)}${
    538                 if (name != null) {
    539                     "?receiver-name=${name.encodeURLParameter()}"
    540                 } else {
    541                     ""
    542                 }
    543             }"
    544         ).expectIban()
    545     }
    546 }
    547 
    548 class XTalerBankPayto internal constructor(
    549     override val parsed: Url,
    550     override val canonical: String,
    551     override val amount: TalerAmount?,
    552     override val message: String?,
    553     override val receiverName: String?,
    554     val username: String
    555 ) : Payto() {
    556     override fun toString(): String = parsed.toString()
    557 
    558     companion object {
    559         fun forUsername(username: String): XTalerBankPayto {
    560             return parse("payto://x-taler-bank/hostname/$username").expectXTalerBank()
    561         }
    562     }
    563 }
    564 
    565 /** Context specific data necessary to create a bank payto URI from a canonical payto URI */
    566 data class BankPaytoCtx(
    567     val bic: String?,
    568     val hostname: String
    569 )
    570 
    571 
    572 /** 16-byte Crockford's Base32 encoded data */
    573 @Serializable(with = Base32Crockford16B.Serializer::class)
    574 class Base32Crockford16B {
    575     private var encoded: String? = null
    576     val raw: ByteArray
    577 
    578     constructor(encoded: String) {
    579         val decoded = try {
    580             Base32Crockford.decode(encoded)
    581         } catch (e: IllegalArgumentException) {
    582             null
    583         }
    584         require(decoded != null && decoded.size == 16) {
    585             "expected 16 bytes encoded in Crockford's base32"
    586         }
    587         this.raw = decoded
    588         this.encoded = encoded
    589     }
    590 
    591     constructor(raw: ByteArray) {
    592         require(raw.size == 16) {
    593             "encoded data should be 16 bytes long"
    594         }
    595         this.raw = raw
    596     }
    597 
    598     fun encoded(): String {
    599         val tmp = encoded ?: Base32Crockford.encode(raw)
    600         encoded = tmp
    601         return tmp
    602     }
    603 
    604     override fun toString(): String {
    605         return encoded()
    606     }
    607 
    608     override fun equals(other: Any?) = (other is Base32Crockford16B) && raw.contentEquals(other.raw)
    609 
    610     internal object Serializer : KSerializer<Base32Crockford16B> {
    611         override val descriptor: SerialDescriptor =
    612             PrimitiveSerialDescriptor("Base32Crockford16B", PrimitiveKind.STRING)
    613 
    614         override fun serialize(encoder: Encoder, value: Base32Crockford16B) {
    615             encoder.encodeString(value.encoded())
    616         }
    617 
    618         override fun deserialize(decoder: Decoder): Base32Crockford16B {
    619             return Base32Crockford16B(decoder.decodeString())
    620         }
    621     }
    622 
    623     companion object {
    624         fun rand(): Base32Crockford16B = Base32Crockford16B(ByteArray(16).rand())
    625         fun secureRand(): Base32Crockford16B = Base32Crockford16B(ByteArray(16).secureRand())
    626     }
    627 }
    628 
    629 /** 32-byte Crockford's Base32 encoded data */
    630 @Description("32-byte Crockford Base32 encoded data")
    631 @Serializable(with = Base32Crockford32B.Serializer::class)
    632 class Base32Crockford32B {
    633     private var encoded: String? = null
    634     val raw: ByteArray
    635 
    636     constructor(encoded: String) {
    637         val decoded = try {
    638             Base32Crockford.decode(encoded)
    639         } catch (e: IllegalArgumentException) {
    640             null
    641         }
    642         require(decoded != null && decoded.size == 32) {
    643             "expected 32 bytes encoded in Crockford's base32"
    644         }
    645         this.raw = decoded
    646         this.encoded = encoded
    647     }
    648 
    649     constructor(raw: ByteArray) {
    650         require(raw.size == 32) {
    651             "encoded data should be 32 bytes long"
    652         }
    653         this.raw = raw
    654     }
    655 
    656     fun encoded(): String {
    657         val tmp = encoded ?: Base32Crockford.encode(raw)
    658         encoded = tmp
    659         return tmp
    660     }
    661 
    662     override fun toString(): String {
    663         return encoded()
    664     }
    665 
    666     override fun equals(other: Any?) = (other is Base32Crockford32B) && raw.contentEquals(other.raw)
    667 
    668     internal object Serializer : KSerializer<Base32Crockford32B> {
    669         override val descriptor: SerialDescriptor =
    670             PrimitiveSerialDescriptor("Base32Crockford32B", PrimitiveKind.STRING)
    671 
    672         override fun serialize(encoder: Encoder, value: Base32Crockford32B) {
    673             encoder.encodeString(value.encoded())
    674         }
    675 
    676         override fun deserialize(decoder: Decoder): Base32Crockford32B {
    677             return Base32Crockford32B(decoder.decodeString())
    678         }
    679     }
    680 
    681     companion object {
    682         fun rand(): Base32Crockford32B = Base32Crockford32B(ByteArray(32).rand())
    683         fun secureRand(): Base32Crockford32B = Base32Crockford32B(ByteArray(32).secureRand())
    684         fun randEdsaKey(): EddsaPublicKey = randEdsaKeyPair().second
    685         fun randEdsaKeyPair(): Pair<ByteArray, EddsaPublicKey> {
    686             val secretKey = ByteArray(32)
    687             Ed25519.generatePrivateKey(SECURE_RNG.get(), secretKey)
    688             val publicKey = ByteArray(32)
    689             Ed25519.generatePublicKey(secretKey, 0, publicKey, 0)
    690             return Pair(secretKey, Base32Crockford32B(publicKey))
    691         }
    692     }
    693 }
    694 
    695 /** 64-byte Crockford's Base32 encoded data */
    696 @Description("64-byte Crockford Base32 encoded data")
    697 @Serializable(with = Base32Crockford64B.Serializer::class)
    698 class Base32Crockford64B {
    699     private var encoded: String? = null
    700     val raw: ByteArray
    701 
    702     constructor(encoded: String) {
    703         val decoded = try {
    704             Base32Crockford.decode(encoded)
    705         } catch (e: IllegalArgumentException) {
    706             null
    707         }
    708 
    709         require(decoded != null && decoded.size == 64) {
    710             "expected 64 bytes encoded in Crockford's base32"
    711         }
    712         this.raw = decoded
    713         this.encoded = encoded
    714     }
    715 
    716     constructor(raw: ByteArray) {
    717         require(raw.size == 64) {
    718             "encoded data should be 64 bytes long"
    719         }
    720         this.raw = raw
    721     }
    722 
    723     fun encoded(): String {
    724         val tmp = encoded ?: Base32Crockford.encode(raw)
    725         encoded = tmp
    726         return tmp
    727     }
    728 
    729     override fun toString(): String {
    730         return encoded()
    731     }
    732 
    733     override fun equals(other: Any?) = (other is Base32Crockford64B) && raw.contentEquals(other.raw)
    734 
    735     internal object Serializer : KSerializer<Base32Crockford64B> {
    736         override val descriptor: SerialDescriptor =
    737             PrimitiveSerialDescriptor("Base32Crockford64B", PrimitiveKind.STRING)
    738 
    739         override fun serialize(encoder: Encoder, value: Base32Crockford64B) {
    740             encoder.encodeString(value.encoded())
    741         }
    742 
    743         override fun deserialize(decoder: Decoder): Base32Crockford64B {
    744             return Base32Crockford64B(decoder.decodeString())
    745         }
    746     }
    747 
    748     companion object {
    749         fun rand(): Base32Crockford64B = Base32Crockford64B(ByteArray(64).rand())
    750     }
    751 }
    752 
    753 /** 32-byte hash code */
    754 typealias ShortHashCode = Base32Crockford32B
    755 /** 64-byte hash code */
    756 typealias HashCode = Base32Crockford64B
    757 
    758 typealias EddsaSignature = Base32Crockford64B
    759 /**
    760  * EdDSA and ECDHE public keys always point on Curve25519
    761  * and represented  using the standard 256 bits Ed25519 compact format,
    762  * converted to Crockford Base32.
    763  */
    764 typealias EddsaPublicKey = Base32Crockford32B