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