libeufin

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

TalerCommon.kt (27203B)


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