TalerCommon.kt (27038B)
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.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 companion object { 143 fun never(): TalerTimestamp = TalerTimestamp(Instant.MAX) 144 } 145 } 146 147 @JvmInline 148 @Serializable(with = BaseURL.Serializer::class) 149 value class BaseURL private constructor(val url: URL) { 150 companion object { 151 fun parse(raw: String): BaseURL { 152 val url = URI(raw).toURL() 153 if (url.protocol !in setOf("http", "https")) { 154 throw badRequest("only 'http' and 'https' are accepted for baseURL got '${url.protocol}'") 155 } else if (url.host.isNullOrBlank()) { 156 throw badRequest("missing host in baseURL got '${url}'") 157 } else if (url.query != null) { 158 throw badRequest("require no query in baseURL got '${url.query}'") 159 } else if (url.ref != null) { 160 throw badRequest("require no fragments in baseURL got '${url.ref}'") 161 } else if (!url.path.endsWith('/')) { 162 throw badRequest("baseURL path must end with / got '${url.path}'") 163 } 164 return BaseURL(url) 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: URI 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 fun parse(raw: String): Payto { 425 val parsed = try { 426 URI(raw) 427 } catch (e: Exception) { 428 throw CommonError.Payto("expected a valid URI") 429 } 430 if (parsed.scheme != "payto") throw CommonError.Payto("expect a payto URI got '${parsed.scheme}'") 431 432 val params = parseQueryString(parsed.query ?: "") 433 val amount = params["amount"]?.run { TalerAmount(this) } 434 val message = params["message"] 435 val receiverName = params["receiver-name"] 436 437 return when (parsed.host) { 438 "iban" -> { 439 val splitPath = parsed.path.split("/", limit = 3).filter { it.isNotEmpty() } 440 val (bic, rawIban) = when (splitPath.size) { 441 1 -> Pair(null, splitPath[0]) 442 2 -> Pair(splitPath[0], splitPath[1]) 443 else -> throw CommonError.Payto("too many path segments for an IBAN payto URI") 444 } 445 val iban = IBAN.parse(rawIban) 446 IbanPayto( 447 parsed, 448 "payto://iban/$iban", 449 amount, 450 message, 451 receiverName, 452 bic, 453 iban 454 ) 455 } 456 457 "x-taler-bank" -> { 458 val splitPath = parsed.path.split("/", limit = 3).filter { it.isNotEmpty() } 459 if (splitPath.size != 2) 460 throw CommonError.Payto("bad number of path segments for a x-taler-bank payto URI") 461 val username = splitPath[1] 462 XTalerBankPayto( 463 parsed, 464 "payto://x-taler-bank/localhost/$username", 465 amount, 466 message, 467 receiverName, 468 username 469 ) 470 } 471 472 else -> throw CommonError.Payto("unsupported payto URI kind '${parsed.host}'") 473 } 474 } 475 } 476 } 477 478 @Serializable(with = IbanPayto.Serializer::class) 479 class IbanPayto internal constructor( 480 override val parsed: URI, 481 override val canonical: String, 482 override val amount: TalerAmount?, 483 override val message: String?, 484 override val receiverName: String?, 485 val bic: String?, 486 val iban: IBAN 487 ) : Payto() { 488 override fun toString(): String = parsed.toString() 489 490 /** Format an IbanPayto in a more human readable way */ 491 fun fmt(): String = buildString { 492 append('(') 493 append(iban) 494 if (bic != null) { 495 append(' ') 496 append(bic) 497 } 498 if (receiverName != null) { 499 append(' ') 500 append(receiverName) 501 } 502 append(')') 503 } 504 505 /** Transform an IBAN payto URI to its simple form without any query */ 506 fun simple(): String = build(iban.toString(), bic, null) 507 508 /** Transform an IBAN payto URI to its full form, using [name] as its receiver-name */ 509 fun full(name: String): String = build(iban.toString(), bic, name) 510 511 internal object Serializer : KSerializer<IbanPayto> { 512 override val descriptor: SerialDescriptor = 513 PrimitiveSerialDescriptor("IbanPayto", PrimitiveKind.STRING) 514 515 override fun serialize(encoder: Encoder, value: IbanPayto) { 516 encoder.encodeString(value.toString()) 517 } 518 519 override fun deserialize(decoder: Decoder): IbanPayto { 520 return parse(decoder.decodeString()).expectIban() 521 } 522 } 523 524 companion object { 525 fun build(iban: String, bic: String?, name: String?): String { 526 val bic = if (bic != null) "$bic/" else "" 527 val name = if (name != null) "?receiver-name=${name.encodeURLParameter()}" else "" 528 return "payto://iban/$bic$iban$name" 529 } 530 531 fun rand(name: String? = null): IbanPayto = parse( 532 "payto://iban/SANDBOXX/${IBAN.rand(Country.DE)}${ 533 if (name != null) { 534 "?receiver-name=${name.encodeURLParameter()}" 535 } else { 536 "" 537 } 538 }" 539 ).expectIban() 540 } 541 } 542 543 class XTalerBankPayto internal constructor( 544 override val parsed: URI, 545 override val canonical: String, 546 override val amount: TalerAmount?, 547 override val message: String?, 548 override val receiverName: String?, 549 val username: String 550 ) : Payto() { 551 override fun toString(): String = parsed.toString() 552 553 companion object { 554 fun forUsername(username: String): XTalerBankPayto { 555 return parse("payto://x-taler-bank/hostname/$username").expectXTalerBank() 556 } 557 } 558 } 559 560 /** Context specific data necessary to create a bank payto URI from a canonical payto URI */ 561 data class BankPaytoCtx( 562 val bic: String?, 563 val hostname: String 564 ) 565 566 567 /** 16-byte Crockford's Base32 encoded data */ 568 @Serializable(with = Base32Crockford16B.Serializer::class) 569 class Base32Crockford16B { 570 private var encoded: String? = null 571 val raw: ByteArray 572 573 constructor(encoded: String) { 574 val decoded = try { 575 Base32Crockford.decode(encoded) 576 } catch (e: IllegalArgumentException) { 577 null 578 } 579 require(decoded != null && decoded.size == 16) { 580 "expected 16 bytes encoded in Crockford's base32" 581 } 582 this.raw = decoded 583 this.encoded = encoded 584 } 585 586 constructor(raw: ByteArray) { 587 require(raw.size == 16) { 588 "encoded data should be 16 bytes long" 589 } 590 this.raw = raw 591 } 592 593 fun encoded(): String { 594 val tmp = encoded ?: Base32Crockford.encode(raw) 595 encoded = tmp 596 return tmp 597 } 598 599 override fun toString(): String { 600 return encoded() 601 } 602 603 override fun equals(other: Any?) = (other is Base32Crockford16B) && raw.contentEquals(other.raw) 604 605 internal object Serializer : KSerializer<Base32Crockford16B> { 606 override val descriptor: SerialDescriptor = 607 PrimitiveSerialDescriptor("Base32Crockford16B", PrimitiveKind.STRING) 608 609 override fun serialize(encoder: Encoder, value: Base32Crockford16B) { 610 encoder.encodeString(value.encoded()) 611 } 612 613 override fun deserialize(decoder: Decoder): Base32Crockford16B { 614 return Base32Crockford16B(decoder.decodeString()) 615 } 616 } 617 618 companion object { 619 fun rand(): Base32Crockford16B = Base32Crockford16B(ByteArray(16).rand()) 620 fun secureRand(): Base32Crockford16B = Base32Crockford16B(ByteArray(16).secureRand()) 621 } 622 } 623 624 /** 32-byte Crockford's Base32 encoded data */ 625 @Serializable(with = Base32Crockford32B.Serializer::class) 626 class Base32Crockford32B { 627 private var encoded: String? = null 628 val raw: ByteArray 629 630 constructor(encoded: String) { 631 val decoded = try { 632 Base32Crockford.decode(encoded) 633 } catch (e: IllegalArgumentException) { 634 null 635 } 636 require(decoded != null && decoded.size == 32) { 637 "expected 32 bytes encoded in Crockford's base32" 638 } 639 this.raw = decoded 640 this.encoded = encoded 641 } 642 643 constructor(raw: ByteArray) { 644 require(raw.size == 32) { 645 "encoded data should be 32 bytes long" 646 } 647 this.raw = raw 648 } 649 650 fun encoded(): String { 651 val tmp = encoded ?: Base32Crockford.encode(raw) 652 encoded = tmp 653 return tmp 654 } 655 656 override fun toString(): String { 657 return encoded() 658 } 659 660 override fun equals(other: Any?) = (other is Base32Crockford32B) && raw.contentEquals(other.raw) 661 662 internal object Serializer : KSerializer<Base32Crockford32B> { 663 override val descriptor: SerialDescriptor = 664 PrimitiveSerialDescriptor("Base32Crockford32B", PrimitiveKind.STRING) 665 666 override fun serialize(encoder: Encoder, value: Base32Crockford32B) { 667 encoder.encodeString(value.encoded()) 668 } 669 670 override fun deserialize(decoder: Decoder): Base32Crockford32B { 671 return Base32Crockford32B(decoder.decodeString()) 672 } 673 } 674 675 companion object { 676 fun rand(): Base32Crockford32B = Base32Crockford32B(ByteArray(32).rand()) 677 fun secureRand(): Base32Crockford32B = Base32Crockford32B(ByteArray(32).secureRand()) 678 fun randEdsaKey(): EddsaPublicKey = randEdsaKeyPair().second 679 fun randEdsaKeyPair(): Pair<ByteArray, EddsaPublicKey> { 680 val secretKey = ByteArray(32) 681 Ed25519.generatePrivateKey(SECURE_RNG.get(), secretKey) 682 val publicKey = ByteArray(32) 683 Ed25519.generatePublicKey(secretKey, 0, publicKey, 0) 684 return Pair(secretKey, Base32Crockford32B(publicKey)) 685 } 686 } 687 } 688 689 /** 64-byte Crockford's Base32 encoded data */ 690 @Serializable(with = Base32Crockford64B.Serializer::class) 691 class Base32Crockford64B { 692 private var encoded: String? = null 693 val raw: ByteArray 694 695 constructor(encoded: String) { 696 val decoded = try { 697 Base32Crockford.decode(encoded) 698 } catch (e: IllegalArgumentException) { 699 null 700 } 701 702 require(decoded != null && decoded.size == 64) { 703 "expected 64 bytes encoded in Crockford's base32" 704 } 705 this.raw = decoded 706 this.encoded = encoded 707 } 708 709 constructor(raw: ByteArray) { 710 require(raw.size == 64) { 711 "encoded data should be 64 bytes long" 712 } 713 this.raw = raw 714 } 715 716 fun encoded(): String { 717 val tmp = encoded ?: Base32Crockford.encode(raw) 718 encoded = tmp 719 return tmp 720 } 721 722 override fun toString(): String { 723 return encoded() 724 } 725 726 override fun equals(other: Any?) = (other is Base32Crockford64B) && raw.contentEquals(other.raw) 727 728 private object Serializer : KSerializer<Base32Crockford64B> { 729 override val descriptor: SerialDescriptor = 730 PrimitiveSerialDescriptor("Base32Crockford64B", PrimitiveKind.STRING) 731 732 override fun serialize(encoder: Encoder, value: Base32Crockford64B) { 733 encoder.encodeString(value.encoded()) 734 } 735 736 override fun deserialize(decoder: Decoder): Base32Crockford64B { 737 return Base32Crockford64B(decoder.decodeString()) 738 } 739 } 740 741 companion object { 742 fun rand(): Base32Crockford64B = Base32Crockford64B(ByteArray(64).rand()) 743 } 744 } 745 746 /** 32-byte hash code */ 747 typealias ShortHashCode = Base32Crockford32B 748 /** 64-byte hash code */ 749 typealias HashCode = Base32Crockford64B 750 751 typealias EddsaSignature = Base32Crockford64B 752 /** 753 * EdDSA and ECDHE public keys always point on Curve25519 754 * and represented using the standard 256 bits Ed25519 compact format, 755 * converted to Crockford Base32. 756 */ 757 typealias EddsaPublicKey = Base32Crockford32B