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