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