libeufin

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

TalerCommon.kt (26712B)


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