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