libeufin

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

TalerCommon.kt (28090B)


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