summaryrefslogtreecommitdiff
path: root/wallet/src
diff options
context:
space:
mode:
Diffstat (limited to 'wallet/src')
-rw-r--r--wallet/src/androidMain/AndroidManifest.xml23
-rw-r--r--wallet/src/androidMain/kotlin/net/taler/wallet/kotlin/Db.kt23
-rw-r--r--wallet/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt132
-rw-r--r--wallet/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt136
-rw-r--r--wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/TestUtilsAndroid.kt24
-rw-r--r--wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/PlanchetTest.kt125
-rw-r--r--wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt481
-rw-r--r--wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RsaBlindingTest.kt114
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Amount.kt210
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Base32Crockford.kt129
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt89
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/PaytoUri.kt45
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/TalerUri.kt60
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Time.kt85
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt96
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt41
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Version.kt78
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/WalletApi.kt101
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Crypto.kt78
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoImpl.kt55
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Deposit.kt110
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Kdf.kt90
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt87
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Recoup.kt83
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt265
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt155
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Auditor.kt56
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Denomination.kt234
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Exchange.kt262
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/ExchangeRecord.kt151
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Keys.kt97
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Wire.kt79
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/operations/Withdraw.kt305
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/AmountTest.kt276
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/Base32CrockfordTest.kt123
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt100
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/PaytoUriTest.kt58
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/TalerUriTest.kt65
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt69
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/TimestampTest.kt75
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/VersionTest.kt57
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/WalletApiTest.kt93
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/DepositTest.kt90
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/EllipticCurveTest.kt156
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/KdfTest.kt213
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/RecoupTest.kt90
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshPlanchetTest.kt112
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/Sha256Test.kt70
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/Sha512Test.kt116
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt493
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/DenominationTest.kt91
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/Denominations.kt119
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/KeysTest.kt316
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/UpdateTest.kt37
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/WireTest.kt175
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/operations/WithdrawTest.kt140
-rw-r--r--wallet/src/jsMain/kotlin/net/taler/wallet/kotlin/Db.kt23
-rw-r--r--wallet/src/jsMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt185
-rw-r--r--wallet/src/jsTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt25
-rw-r--r--wallet/src/nativeInterop/cinterop/sodium-static.def6
-rw-r--r--wallet/src/nativeInterop/cinterop/sodium.def4
-rw-r--r--wallet/src/nativeMain/kotlin/net/taler/wallet/kotlin/Db.kt23
-rw-r--r--wallet/src/nativeMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt187
-rw-r--r--wallet/src/nativeTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt24
64 files changed, 7710 insertions, 0 deletions
diff --git a/wallet/src/androidMain/AndroidManifest.xml b/wallet/src/androidMain/AndroidManifest.xml
new file mode 100644
index 0000000..77fbf4a
--- /dev/null
+++ b/wallet/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2020 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="net.taler.wallet.kotlin">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.INTERNET" />
+
+</manifest>
diff --git a/wallet/src/androidMain/kotlin/net/taler/wallet/kotlin/Db.kt b/wallet/src/androidMain/kotlin/net/taler/wallet/kotlin/Db.kt
new file mode 100644
index 0000000..45cbfc3
--- /dev/null
+++ b/wallet/src/androidMain/kotlin/net/taler/wallet/kotlin/Db.kt
@@ -0,0 +1,23 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+internal actual class DbFactory {
+ actual fun openDb(): Db {
+ return FakeDb()
+ }
+}
diff --git a/wallet/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt b/wallet/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
new file mode 100644
index 0000000..502b8a0
--- /dev/null
+++ b/wallet/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
@@ -0,0 +1,132 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import com.goterl.lazycode.lazysodium.LazySodiumJava
+import com.goterl.lazycode.lazysodium.SodiumJava
+import com.goterl.lazycode.lazysodium.interfaces.Hash
+import com.goterl.lazycode.lazysodium.interfaces.Hash.State512
+import com.goterl.lazycode.lazysodium.interfaces.KeyExchange
+import com.goterl.lazycode.lazysodium.interfaces.Sign
+import com.goterl.lazycode.lazysodium.utils.Key
+
+internal actual object CryptoFactory {
+ internal actual fun getCrypto(): Crypto = CryptoJvmImpl
+}
+
+internal object CryptoJvmImpl : CryptoImpl() {
+
+ private val sodium = LazySodiumJava(SodiumJava())
+
+ override fun sha256(input: ByteArray): ByteArray {
+ val output = ByteArray(Hash.SHA256_BYTES)
+ sodium.cryptoHashSha256(output, input, input.size.toLong())
+ return output
+ }
+
+ override fun sha512(input: ByteArray): ByteArray {
+ val output = ByteArray(Hash.SHA512_BYTES)
+ sodium.cryptoHashSha512(output, input, input.size.toLong())
+ return output
+ }
+
+ override fun getHashSha512State(): HashSha512State {
+ return JvmHashSha512State()
+ }
+
+ override fun getRandomBytes(num: Int): ByteArray {
+ return sodium.randomBytesBuf(num)
+ }
+
+ override fun eddsaGetPublic(eddsaPrivateKey: ByteArray): ByteArray {
+ return sodium.cryptoSignSeedKeypair(eddsaPrivateKey).publicKey.asBytes
+ }
+
+ override fun ecdheGetPublic(ecdhePrivateKey: ByteArray): ByteArray {
+ return sodium.cryptoScalarMultBase(Key.fromBytes(ecdhePrivateKey)).asBytes
+ }
+
+ override fun createEddsaKeyPair(): EddsaKeyPair {
+ val privateKey = sodium.randomBytesBuf(KeyExchange.SEEDBYTES)
+ val publicKey = eddsaGetPublic(privateKey)
+ return EddsaKeyPair(privateKey, publicKey)
+ }
+
+ override fun createEcdheKeyPair(): EcdheKeyPair {
+ val privateKey = sodium.randomBytesBuf(KeyExchange.SEEDBYTES)
+ val publicKey = ecdheGetPublic(privateKey)
+ return EcdheKeyPair(privateKey, publicKey)
+ }
+
+ override fun eddsaSign(msg: ByteArray, eddsaPrivateKey: ByteArray): ByteArray {
+ val privateKey = sodium.cryptoSignSeedKeypair(eddsaPrivateKey).secretKey.asBytes
+ val signatureBytes = ByteArray(Sign.BYTES)
+ sodium.cryptoSignDetached(signatureBytes, msg, msg.size.toLong(), privateKey)
+ return signatureBytes
+ }
+
+ override fun eddsaVerify(msg: ByteArray, sig: ByteArray, eddsaPub: ByteArray): Boolean {
+ return sodium.cryptoSignVerifyDetached(sig, msg, msg.size, eddsaPub)
+ }
+
+ override fun keyExchangeEddsaEcdhe(eddsaPrivateKey: ByteArray, ecdhePublicKey: ByteArray): ByteArray {
+ val ph = sha512(eddsaPrivateKey)
+ val a = ph.copyOfRange(0, 32)
+ val x = sodium.cryptoScalarMult(Key.fromBytes(a), Key.fromBytes(ecdhePublicKey)).asBytes
+ return sha512(x)
+ }
+
+ override fun keyExchangeEcdheEddsa(ecdhePrivateKey: ByteArray, eddsaPublicKey: ByteArray): ByteArray {
+ val curve25519Pub = ByteArray(KeyExchange.PUBLICKEYBYTES)
+ sodium.convertPublicKeyEd25519ToCurve25519(curve25519Pub, eddsaPublicKey)
+ val x = sodium.cryptoScalarMult(Key.fromBytes(ecdhePrivateKey), Key.fromBytes(curve25519Pub)).asBytes
+ return sha512(x)
+ }
+
+ override fun rsaBlind(hm: ByteArray, bks: ByteArray, rsaPubEnc: ByteArray): ByteArray {
+ return RsaBlinding.rsaBlind(hm, bks, rsaPubEnc)
+ }
+
+ override fun rsaUnblind(sig: ByteArray, rsaPubEnc: ByteArray, bks: ByteArray): ByteArray {
+ return RsaBlinding.rsaUnblind(sig, rsaPubEnc, bks)
+ }
+
+ override fun rsaVerify(hm: ByteArray, rsaSig: ByteArray, rsaPubEnc: ByteArray): Boolean {
+ return RsaBlinding.rsaVerify(hm, rsaSig, rsaPubEnc)
+ }
+
+ private class JvmHashSha512State : HashSha512State {
+ private val state = State512()
+
+ init {
+ check(sodium.cryptoHashSha512Init(state)) { "Error doing cryptoHashSha512Init" }
+ }
+
+ override fun update(data: ByteArray): HashSha512State {
+ sodium.cryptoHashSha512Update(state, data, data.size.toLong())
+ return this
+ }
+
+ override fun final(): ByteArray {
+ val output = ByteArray(Hash.SHA512_BYTES)
+ sodium.cryptoHashSha512Final(state, output)
+ return output
+ }
+
+ }
+
+}
diff --git a/wallet/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt b/wallet/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt
new file mode 100644
index 0000000..458a089
--- /dev/null
+++ b/wallet/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt
@@ -0,0 +1,136 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import java.math.BigInteger
+import kotlin.math.abs
+import kotlin.math.ceil
+import kotlin.math.floor
+
+internal object RsaBlinding {
+
+ fun rsaBlind(hm: ByteArray, bks: ByteArray, rsaPubEnc: ByteArray): ByteArray {
+ val rsaPub = rsaPubDecode(rsaPubEnc)
+ val data = rsaFullDomainHash(hm, rsaPub)
+ val r = rsaBlindingKeyDerive(rsaPub, bks)
+ val rE = r.modPow(rsaPub.e, rsaPub.n)
+ val bm = rE.multiply(data).mod(rsaPub.n)
+ return bm.toByteArrayWithoutSign()
+ }
+
+ fun rsaUnblind(sig: ByteArray, rsaPubEnc: ByteArray, bks: ByteArray): ByteArray {
+ val rsaPub = rsaPubDecode(rsaPubEnc)
+ val blindedSig = BigInteger(1, sig)
+ val r = rsaBlindingKeyDerive(rsaPub, bks)
+ val rInv = r.modInverse(rsaPub.n)
+ val s = blindedSig.multiply(rInv).mod(rsaPub.n)
+ return s.toByteArrayWithoutSign()
+ }
+
+ fun rsaVerify(hm: ByteArray, rsaSig: ByteArray, rsaPubEnc: ByteArray): Boolean {
+ val rsaPub = rsaPubDecode(rsaPubEnc)
+ val d = rsaFullDomainHash(hm, rsaPub)
+ val sig = BigInteger(1, rsaSig)
+ val sigE = sig.modPow(rsaPub.e, rsaPub.n)
+ return sigE == d
+ }
+
+ private fun rsaBlindingKeyDerive(rsaPub: RsaPublicKey, bks: ByteArray): BigInteger {
+ val salt = "Blinding KDF extrator HMAC key".encodeToByteArray()
+ val info = "Blinding KDF".encodeToByteArray()
+ return kdfMod(rsaPub.n, bks, salt, info)
+ }
+
+ private fun rsaPubDecode(publicKey: ByteArray): RsaPublicKey {
+ val modulusLength = abs((publicKey[0].toInt() shl 8) or publicKey[1].toInt())
+ val exponentLength = abs((publicKey[2].toInt() shl 8) or publicKey[3].toInt())
+ if (4 + exponentLength + modulusLength != publicKey.size) {
+ throw Error("invalid RSA public key (format wrong)")
+ }
+ val modulus = publicKey.copyOfRange(4, 4 + modulusLength)
+ val exponent = publicKey.copyOfRange(
+ 4 + modulusLength,
+ 4 + modulusLength + exponentLength
+ )
+ return RsaPublicKey(BigInteger(1, modulus), BigInteger(1, exponent))
+ }
+
+ private fun rsaFullDomainHash(hm: ByteArray, rsaPublicKey: RsaPublicKey): BigInteger {
+ val info = "RSA-FDA FTpsW!".encodeToByteArray()
+ val salt = rsaPubEncode(rsaPublicKey)
+ val r = kdfMod(rsaPublicKey.n, hm, salt, info)
+ rsaGcdValidate(r, rsaPublicKey.n)
+ return r
+ }
+
+ private fun rsaPubEncode(rsaPublicKey: RsaPublicKey): ByteArray {
+ val mb = rsaPublicKey.n.toByteArrayWithoutSign()
+ val eb = rsaPublicKey.e.toByteArrayWithoutSign()
+ val out = ByteArray(4 + mb.size + eb.size)
+ out[0] = ((mb.size ushr 8) and 0xff).toByte()
+ out[1] = (mb.size and 0xff).toByte()
+ out[2] = ((eb.size ushr 8) and 0xff).toByte()
+ out[3] = (eb.size and 0xff).toByte()
+ mb.copyInto(out, destinationOffset = 4)
+ eb.copyInto(out, destinationOffset = 4 + mb.size)
+ return out
+ }
+
+ private fun kdfMod(n: BigInteger, ikm: ByteArray, salt: ByteArray, info: ByteArray): BigInteger {
+ val nBits = n.bitLength()
+ val bufLen = floor((nBits.toDouble() - 1) / 8 + 1).toInt()
+ val mask = (1 shl (8 - (bufLen * 8 - nBits))) - 1
+ var counter = 0
+ while (true) {
+ val ctx = ByteArray(info.size + 2)
+ info.copyInto(ctx)
+ ctx[ctx.size - 2] = ((counter ushr 8) and 0xff).toByte()
+ ctx[ctx.size - 1] = (counter and 0xff).toByte()
+ val buf = CryptoJvmImpl.kdf(bufLen, ikm, salt, ctx)
+ val arr = buf.copyOf()
+ arr[0] = (arr[0].toInt() and mask).toByte()
+ val r = BigInteger(1, arr)
+ if (r < n) return r
+ counter++
+ }
+ }
+
+ /**
+ * Test for malicious RSA key.
+ *
+ * Assuming n is an RSA modulous and r is generated using a call to
+ * GNUNET_CRYPTO_kdf_mod_mpi, if gcd(r,n) != 1 then n must be a
+ * malicious RSA key designed to deanomize the user.
+ *
+ * @param r KDF result
+ * @param n RSA modulus of the public key
+ */
+ private fun rsaGcdValidate(r: BigInteger, n: BigInteger) {
+ if (r.gcd(n) != BigInteger.ONE) throw Error("malicious RSA public key")
+ }
+
+ // TODO check that this strips *only* the sign correctly
+ private fun BigInteger.toByteArrayWithoutSign(): ByteArray = this.toByteArray().let {
+ val byteLength = ceil(this.bitLength().toDouble() / 8).toInt()
+ val signBitPosition = ceil((this.bitLength() + 1).toDouble() / 8).toInt()
+ val start = signBitPosition - byteLength
+ it.copyOfRange(start, it.size) // stripping least significant byte (sign)
+ }
+
+}
+
+internal class RsaPublicKey(val n: BigInteger, val e: BigInteger)
diff --git a/wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/TestUtilsAndroid.kt b/wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/TestUtilsAndroid.kt
new file mode 100644
index 0000000..a362874
--- /dev/null
+++ b/wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/TestUtilsAndroid.kt
@@ -0,0 +1,24 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.runBlocking
+
+actual fun runCoroutine(block: suspend (scope : CoroutineScope) -> Unit) = runBlocking { block(this) }
+
+actual fun getPlatformTarget(): PlatformTarget = PlatformTarget.ANDROID
diff --git a/wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/PlanchetTest.kt b/wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/PlanchetTest.kt
new file mode 100644
index 0000000..d7f1dae
--- /dev/null
+++ b/wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/PlanchetTest.kt
@@ -0,0 +1,125 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.crypto.Planchet.CreationRequest
+import net.taler.wallet.kotlin.crypto.Planchet.CreationResult
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+
+// TODO move to commonTest once RsaBlinding is implemented everywhere
+class PlanchetTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+ private val planchet = Planchet(crypto)
+
+ private class PlanchetVector(val request: CreationRequest, val eddsaKeyPair: EddsaKeyPair, val blindingFactor: String, val result: CreationResult)
+
+ @Test
+ fun testCreate() {
+ val vectors = listOf(
+ PlanchetVector(
+ CreationRequest(
+ denomPub = "040000XVGVWCHVQVTQ06Q5V0XRAVQKPPZQZ68GYVXSC5RAG37VDCG0CEQHS4876BX6DDABB2WFY7TRJ7MFKTMMDF7A7ZW9PKQ8S3RQ15TVTKWBFGGKBKYSP6CVHNG9AY738NCPC8AFWYGP8J2VJE9HRR7M1GQK19E2M7Q2Y54KCSZ583BTNX275DW6EYYE1KBV4FK009Z621EHF5R87S6VQDSBCKSK15JCH1JYC2VPRHHAEGRA2WYX1HD9KFET0C9G1CZJB1MHZ5Z7Y803YZJH441P3PJJTRB9WCTA03H6M43CJ9MB33BEJ3KR22R8CS0D6QC2E7ZQS5MGBWCF51FK97SHCJW93SAT7VHB3YX5VVDNTW9N3SDW56HNWT11D306H9VN7BTP84T404VF482Y09K4SHEF5704002",
+ feeWithdraw = Amount.fromJSONString("KOADwTb3:9329564218.42023"),
+ reservePriv = "9TM70AKDTS57AWY9JK2J4TMBTMW6K62WHHGZWYDG0VM5ABPZKD40",
+ reservePub = "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0",
+ value = Amount.fromJSONString("KOADwTb3:3932974019564218.48282023")
+ ),
+ EddsaKeyPair(
+ Base32Crockford.decode("GX5DGW3RJ4HMXS53W29TK2667NWA3Z4WB41X7GPAX3WX4036VQGG"),
+ Base32Crockford.decode("Z6DXSXGEQ8C4G50FF6TG6ZKBH11APJ2HSNS21BAR72VY3KV7CC90")
+ ),
+ "H58V73J73HXTA9CPT8ZZ5G7VVKCWZFAE1TMCCV176QBQTPBB2H40",
+ CreationResult(
+ blindingKey = "H58V73J73HXTA9CPT8ZZ5G7VVKCWZFAE1TMCCV176QBQTPBB2H40",
+ coinEv = "AK23AD09K8462T621RPER66WQRNE845JAMBT4Y1AA39M22M5K0DZFPH2P5V9E8RD0VC1Z915WB432D2C1BHKHZGP62X9A424JZRPVTFJGYCFYHH52BG0VTJ58ZK70S52KYC2DW4Z0XHKBW0BW3F8NAGGTTGE6NF6EJ3SXYBVYTN0TDJE7HED3ZEGM34N73656TADK0VNZN04BQQNZYW7WWDGT5A06CZTCS4HTSD74CVNJ70CQQQ1C9D14AA75NJ902K6FC7ANBHFGENZXAYNAC0WQQQ6J7XW0TCC3N39TYSCC7TJVH7FZQEXKE8RDGT873QX4C7XDVV6TNWEBBCPM9AABW9PXBEQSM55DT79GVZ7156MZWJKGZAGNVX1FASAY0J3CW507672300R603MW1RRRC",
+ coinPriv = "GX5DGW3RJ4HMXS53W29TK2667NWA3Z4WB41X7GPAX3WX4036VQGG",
+ coinPub = "Z6DXSXGEQ8C4G50FF6TG6ZKBH11APJ2HSNS21BAR72VY3KV7CC90",
+ coinValue = Amount.fromJSONString("KOADwTb3:3932974019564218.48282023"),
+ denomPub = "040000XVGVWCHVQVTQ06Q5V0XRAVQKPPZQZ68GYVXSC5RAG37VDCG0CEQHS4876BX6DDABB2WFY7TRJ7MFKTMMDF7A7ZW9PKQ8S3RQ15TVTKWBFGGKBKYSP6CVHNG9AY738NCPC8AFWYGP8J2VJE9HRR7M1GQK19E2M7Q2Y54KCSZ583BTNX275DW6EYYE1KBV4FK009Z621EHF5R87S6VQDSBCKSK15JCH1JYC2VPRHHAEGRA2WYX1HD9KFET0C9G1CZJB1MHZ5Z7Y803YZJH441P3PJJTRB9WCTA03H6M43CJ9MB33BEJ3KR22R8CS0D6QC2E7ZQS5MGBWCF51FK97SHCJW93SAT7VHB3YX5VVDNTW9N3SDW56HNWT11D306H9VN7BTP84T404VF482Y09K4SHEF5704002",
+ denomPubHash = "XB6T8NRGSRPWBM2YGS3R0AQYGEMK7PAM3CQRX6XM04B4N48PWRVZ5DG5JTT0NNQAGHN5HTGSCPR06R6B5NJBZ2DT5VZSQRD8FTNFPEG",
+ reservePub = "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0",
+ withdrawSig = "SNTZ4DWRVJBK89YGAZ60EDV0T7BM80MD6J6P88BRKDQFP331CXPSGM45CMCVBB7GR6X2FWQC5EJGR0J8KBR459PSGT18DA5PMQZKG08",
+ coinEvHash = "FW36XSCBJCBQMSTT798CYG363481MASXGH5W73G24D2F9C7J76YZ2644PGQ6346XBYDXW7Z61JJZN2C2Y8152NNKW3NB0DHTMKHZ5BR"
+
+ )
+ ),
+ PlanchetVector(
+ CreationRequest(
+ denomPub = "040000YE5QYTJTCYF7YDWN2ECYAMBNENHQT7YT740XNC88V5F1K4YC2QD94WABBVHZY597B2BTGBD2NJJV028JKJTD1KBPKXF4D87B7ZJYZVQSA4ZB5H1FVPE7X7YQVG668YZ2YY229X29NM4B6QR0G3TH821QBT1J5EDPKS0RP8E6X4654DTAAYBEN14H96E8D1JFVE40773FVVPXXMX7ZXT7TCVC2EZFMZR1HQ2DDXD8KJZ9AEGS1YH4D629Y08T9X2533MS6R4X58VVKHN1YQVKJT2044A0S8B4AKCW2GJHMQM10XC3K7C3D1C841A6R96GRXPC02QVBQSA1D5VY5VG2T4HVC6NKVK5WAXDEYZNKYVPD9AV4MNCYGK23AZWGHX5E16BQTNG47C9DEETP2D87XFC9D04002",
+ feeWithdraw = Amount.fromJSONString("cRai6j:32749022734.44771"),
+ reservePriv = "H58V73J73HXTA9CPT8ZZ5G7VVKCWZFAE1TMCCV176QBQTPBB2H40",
+ reservePub = "G3R433316Z9PW1H8XRSATJWZJNMKPZ3EE20Z386X7CYM29JDFE0G",
+ value = Amount.fromJSONString("cRai6j:166032749022734.69444771")
+ ),
+ EddsaKeyPair(
+ Base32Crockford.decode("5QNA3FX8NA7SETDNEEFJK5W3MNP8AJ8WSBY8FYDVZEYQ1BD21EW0"),
+ Base32Crockford.decode("54NDT04NA3TRA38T0D8TMR52PH1EWQP2S4J279GMQWQHKN4W9850")
+ ),
+ "7EKNT64GV5MX0KHZZNB1NREPWCZ7KF9K815M8CQN3B8AKJYF1JV0",
+ CreationResult(
+ blindingKey = "7EKNT64GV5MX0KHZZNB1NREPWCZ7KF9K815M8CQN3B8AKJYF1JV0",
+ coinEv = "AATPF1TXN84PV5P7HE7274B7KT525MFRSPT62MDNYXJXJ2TDGKTMNGPJRH6CMWBD3QQENAEFNS7CZ7P27CBFN6W3EFCFNAS12EWGM6GTTV643RH3A5YJA2R93G0PZPXW9HZP3KZZYFG6MGCRHMHEXTA7T5WKVH6KWE9SM64X9SVKV856VY7TPPWZ0MKZV24KF6TDJ9QC74D2X2FEBDSK7CEA870JENBXC7PZZWJDN8CVN1ZDY4Q0SV8Y4B0YX6CZZ6KVX10PXW56FQ4SSP34EBZCPXCHRZPCQCQRAJ78H4GBP8Y8394QQV1TRH35JQ20R98JSH0WFNAMPQZ246QY8MRFTAT816EY7FEX74ENNKX8494K476BN9VM6CJ5CD0FZYRFSR7DRC5RG9V84SK71EXEDR",
+ coinPriv = "5QNA3FX8NA7SETDNEEFJK5W3MNP8AJ8WSBY8FYDVZEYQ1BD21EW0",
+ coinPub = "54NDT04NA3TRA38T0D8TMR52PH1EWQP2S4J279GMQWQHKN4W9850",
+ coinValue = Amount.fromJSONString("cRai6j:166032749022734.69444771"),
+ denomPub = "040000YE5QYTJTCYF7YDWN2ECYAMBNENHQT7YT740XNC88V5F1K4YC2QD94WABBVHZY597B2BTGBD2NJJV028JKJTD1KBPKXF4D87B7ZJYZVQSA4ZB5H1FVPE7X7YQVG668YZ2YY229X29NM4B6QR0G3TH821QBT1J5EDPKS0RP8E6X4654DTAAYBEN14H96E8D1JFVE40773FVVPXXMX7ZXT7TCVC2EZFMZR1HQ2DDXD8KJZ9AEGS1YH4D629Y08T9X2533MS6R4X58VVKHN1YQVKJT2044A0S8B4AKCW2GJHMQM10XC3K7C3D1C841A6R96GRXPC02QVBQSA1D5VY5VG2T4HVC6NKVK5WAXDEYZNKYVPD9AV4MNCYGK23AZWGHX5E16BQTNG47C9DEETP2D87XFC9D04002",
+ denomPubHash = "RJKMJ93AJ0NYC7X514FPVJ82ST4GW6WZKGK64R69880XBMMGE7H7R8QW71FGWCTKD3KZPW4D3QM854M4YHMYSZ5K3YEA2S7B2GJ9XTR",
+ reservePub = "G3R433316Z9PW1H8XRSATJWZJNMKPZ3EE20Z386X7CYM29JDFE0G",
+ withdrawSig = "X2015X2KE7Z0Q407QEKQ01TKBVV62QT07V9GJGP8GYH04K09TATB9KJG5K4VZG72Y79M1SM1EETVPARSETMN0J7Q057RB6V2F2B2P1G",
+ coinEvHash = "DZ0TEHNTRCXQB3YDZNQYGA0S4RRNKD96Y0PKMG9QQX1KD534RPNRW526CQ5FWESKDT8AJ8R79A9TD20V3JJG3ZQ5JJCMPK9DTF3A8B0"
+ )
+ )
+ )
+ for (v in vectors) testPlanchetVector(v)
+ }
+
+ private fun testPlanchetVector(v: PlanchetVector) {
+ // test vector should match expected result
+ val blindingFactor = Base32Crockford.decode(v.blindingFactor)
+ assertEquals(v.result, planchet.create(v.request, v.eddsaKeyPair, blindingFactor))
+
+ // different value should produce different signature
+ val diffValue = v.request.value - Amount.min(v.request.value.currency)
+ val requestDiffValue = v.request.copy(value = diffValue)
+ val requestDiffResult = v.result.copy(coinValue = diffValue)
+ val result = planchet.create(requestDiffValue, v.eddsaKeyPair, blindingFactor)
+ assertNotEquals(v.result.withdrawSig, result.withdrawSig)
+ assertNotEquals(requestDiffResult, result)
+
+ // different fee should produce different signature
+ val diffFee = v.request.feeWithdraw - Amount.min(v.request.feeWithdraw.currency)
+ val requestDiffFee = v.request.copy(feeWithdraw = diffFee)
+ val resultDiffFee = planchet.create(requestDiffFee, v.eddsaKeyPair, blindingFactor)
+ assertNotEquals(v.result.withdrawSig, resultDiffFee.withdrawSig)
+ assertNotEquals(v.result, resultDiffFee)
+
+ // different blinding factor should change result
+ val diffBlindingFactor = Random.nextBytes(32)
+ assertNotEquals(v.result, planchet.create(v.request, v.eddsaKeyPair, diffBlindingFactor))
+
+ // different coin keys should change result
+ val diffEddsaKeyPair = crypto.createEddsaKeyPair()
+ assertNotEquals(v.result, planchet.create(v.request, diffEddsaKeyPair, blindingFactor))
+ }
+
+}
diff --git a/wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt b/wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt
new file mode 100644
index 0000000..6cdad75
--- /dev/null
+++ b/wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt
@@ -0,0 +1,481 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.CoinRecord
+import net.taler.wallet.kotlin.CoinSourceType.WITHDRAW
+import net.taler.wallet.kotlin.CoinStatus.DORMANT
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.crypto.Refresh.RefreshPlanchetRecord
+import net.taler.wallet.kotlin.crypto.Refresh.RefreshSessionRecord
+import net.taler.wallet.kotlin.exchange.DenominationRecord
+import net.taler.wallet.kotlin.exchange.DenominationSelectionInfo
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedGood
+import net.taler.wallet.kotlin.exchange.SelectedDenomination
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+// TODO move to commonTest once RsaBlinding is implemented everywhere
+class RefreshTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+ private val refresh = Refresh(crypto)
+
+ private class RefreshVector(
+ val kappa: Int,
+ val meltCoin: CoinRecord,
+ val newCoinDenominations: DenominationSelectionInfo,
+ val meltFee: Amount,
+ val refreshSessionRecord: RefreshSessionRecord,
+ val kappaKeys: List<EcdheKeyPair>
+ )
+
+ @Test
+ fun testCreateRefreshSession() {
+ val vectors = listOf(
+ RefreshVector(
+ kappa = 3,
+ meltCoin = CoinRecord(
+ blindingKey = "N2A8K8XVTMPKNRCEJ1B8GMWJM6TXWRGAN5HWPFGPXS3WY76EKB60",
+ coinPriv = "884XH9DBQT9MA34YKCR7WPEP2HH3J1R9C3G7MSB8C3NHB0XEVZW0",
+ coinPub = "3GSHK9JCTMEAZY7BMC1QQD28ABEJEJJJXHPVF2YXTET97WB81GNG",
+ currentAmount = Amount("TESTKUDOS", 1, 96000000),
+ denomPub = "020000YDCXEZRKA0RR18PBW1HGXAH9HM9PN5TPWSVB8TG9QP4KDNHDVS0BRM9ATZV5AW5238WSNJWXA7V6WHE5QGMKRTSZYZZ6DWNHMZGAKCBC3T1HWKVPBHTRJZK68CGRQQ7A6VBRPCRS26QP8Z89TFRVN9NAQC6ZR0CD6P269XNQXYQAARNVBB74QSHJJXPMB5HE6TQ1HHK6AZRNWG2001",
+ denomPubHash = "9ZSE6E0271SRAZTHS08BYF8TGAY2Y1Q8Y7TYWM3ZM25THE3ZJYNKDCXR72YKVQSES1GXWGQVSF13YZ0Z2RQE5VRBMF79KJQP85R71X8",
+ denomSig = "73XS5TEM2WNQWJJBBMYC77DCV98NKX949KAFNJX9KXW2H9QDE4FM7SEJ7RB6RHX99QJGVT6SD1N24GXQCJY1CZA57RKDP3DK2C9WJ8HZ09Y33MYV33TD7C325GQQJFSSZ1EC5JX06BYNGKB0VEHXAR7KHXVT0Y5MZXSD8X65GRNPFV8B3HKC6BBNGVVXXM28WA7VTA7EWJKM4",
+ exchangeBaseUrl = "example.org", status = DORMANT, coinSource = WITHDRAW, suspended = false
+ ),
+ newCoinDenominations = DenominationSelectionInfo(
+ totalCoinValue = Amount("TESTKUDOS", 1, 81000000),
+ totalWithdrawCost = Amount("TESTKUDOS", 1, 92000000),
+ selectedDenominations = listOf(
+ SelectedDenomination(
+ count = 1,
+ denominationRecord = DenominationRecord(
+ denomPub = "020000YDW0GZQGY9GSKTEESAD1ZS803D576X1HTVJM8CWEBPVGQ4GHMDD5SBQHJ462NPW9FJD437HYW69MJR5N4YABJWYZB6P7CZ7CZR0YD7KY1M7C291BQX4T18DGKCTDEDBMVH2CF4K4NWVA1FFX4AYDB5GRWJ0JBJRRAVQ1ZY8D9PX9TJT9HEVBXAMQQF2ESKG1JX5CXDX7AMY2TG2001",
+ denomPubHash = "ZVZMM6GEFETH1S71BRKFKB03BTSTA8JZH26Q1PZED78NZ50EVVT0DMDBPDN5EBY90SK4AWT0J8CNPD7NB2TSCFMSKAS5V6DK26GEKT0",
+ exchangeBaseUrl = "example.org",
+ feeDeposit = Amount("TESTKUDOS", fraction = 2000000, value = 0),
+ feeRefresh = Amount("TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount("TESTKUDOS", fraction = 2000000, value = 0),
+ isOffered = true,
+ isRevoked = false,
+ masterSig = "1XDNXJXA9CY30Q5BTJW5YN8XSYCCFKKSVGR9TW5CFPD4YSTYY52YBHDEPEAX3AY1HWR0XHYQHRJCX156NZMAPQ4BPZ00EXTW0MA1018",
+ stampExpireDeposit = Timestamp(1656768984000),
+ stampExpireLegal = Timestamp(1688304984000),
+ stampExpireWithdraw = Timestamp(1594301784000),
+ stampStart = Timestamp(1593696984000),
+ status = VerifiedGood,
+ value = Amount("TESTKUDOS", fraction = 0, value = 1)
+ )
+ ),
+ SelectedDenomination(
+ count = 8,
+ denominationRecord = DenominationRecord(
+ denomPub = "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ denomPubHash = "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ exchangeBaseUrl = "example.org",
+ feeDeposit = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount("TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ isOffered = true,
+ isRevoked = false,
+ masterSig = "X4WYKHZVAQFRXKCAQ0QCA8VFMC32ESJP8M3BTTYW20SBR46Q1HRF9Y309SQVVH8TY3R8PJ81A4QFAABYEQR2D9RXTSVCBMD667Q723R",
+ stampExpireDeposit = Timestamp(1656768984000),
+ stampExpireLegal = Timestamp(1688304984000),
+ stampExpireWithdraw = Timestamp(1594301784000),
+ stampStart = Timestamp(1593696984000),
+ status = VerifiedGood,
+ value = Amount("TESTKUDOS", fraction = 10000000, value = 0)
+ )
+ ),
+ SelectedDenomination(
+ count = 1,
+ denominationRecord = DenominationRecord(
+ denomPub = "020000Z46ESSPKMQ5875ZZMX9EZ4ZD7MQ3MPM4R2QJ7K7ADMCK0CJDM7W6GEZF0BQGWHVCMA4CY83NBYKBZYB9AYG293JD7VW7BSQTFH51HK02Q96JRTXBAY2DS8ZEXQFXRM6SD43C6BJCBDRCSXXEXA1WG2FFG5CX65TZW94274CMHJNVAY6VR8Y8XKV928P7AHBK106R6TCTS164CG2001",
+ denomPubHash = "0F44ZC9M6XB828XW09DY2YZNM07DA0CZV72D69F31RXH1CQHF3GDPVGZT5135BJEHVX0RGN44SCWS8CR8E39TDSRW10GZ6AEB2R40E8",
+ exchangeBaseUrl = "example.org",
+ feeDeposit = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefund = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ isOffered = true,
+ isRevoked = false,
+ masterSig = "8DGF50VWCB2JHM613NRJM0NQYAVWFXCSBAHKTG3ZHKQ7N59WXK7MA4V794A97YDG0AVP350B2NT1QQ66VS1H2QG3P29PM5QDE6ZH60R",
+ stampExpireDeposit = Timestamp(1656768984000),
+ stampExpireLegal = Timestamp(1688304984000),
+ stampExpireWithdraw = Timestamp(1594301784000),
+ stampStart = Timestamp(1593696984000),
+ status = VerifiedGood,
+ value = Amount("TESTKUDOS", fraction = 1000000, value = 0)
+ )
+ )
+ )
+ ),
+ meltFee = Amount("TESTKUDOS", fraction = 3000000, value = 0),
+ refreshSessionRecord = RefreshSessionRecord(
+ confirmSig = "97N1DQ1FGXT6SFR4J8H9WXCNG741BE986E3YW59T6R4DZ1FPC3XRFQ9D6BNRHGTAANZQXH4T1KXH61DP8BGHY6SYHZJWVGSQBV0A00G",
+ exchangeBaseUrl = "example.org",
+ hash = "XCSSAYNJ6964PVG393PDEGX0CGGZBNY6YMRDNZYBCTBDJF8ASA0BEA5TK435GG1DCFTTK26SS3V0EQ0DB9XKC9M9PWHTE4SZK9JZPVG",
+ meltCoinPub = "3GSHK9JCTMEAZY7BMC1QQD28ABEJEJJJXHPVF2YXTET97WB81GNG",
+ newDenominationHashes = listOf(
+ "ZVZMM6GEFETH1S71BRKFKB03BTSTA8JZH26Q1PZED78NZ50EVVT0DMDBPDN5EBY90SK4AWT0J8CNPD7NB2TSCFMSKAS5V6DK26GEKT0",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "0F44ZC9M6XB828XW09DY2YZNM07DA0CZV72D69F31RXH1CQHF3GDPVGZT5135BJEHVX0RGN44SCWS8CR8E39TDSRW10GZ6AEB2R40E8"
+ ),
+ newDenominations = listOf(
+ "020000YDW0GZQGY9GSKTEESAD1ZS803D576X1HTVJM8CWEBPVGQ4GHMDD5SBQHJ462NPW9FJD437HYW69MJR5N4YABJWYZB6P7CZ7CZR0YD7KY1M7C291BQX4T18DGKCTDEDBMVH2CF4K4NWVA1FFX4AYDB5GRWJ0JBJRRAVQ1ZY8D9PX9TJT9HEVBXAMQQF2ESKG1JX5CXDX7AMY2TG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000Z46ESSPKMQ5875ZZMX9EZ4ZD7MQ3MPM4R2QJ7K7ADMCK0CJDM7W6GEZF0BQGWHVCMA4CY83NBYKBZYB9AYG293JD7VW7BSQTFH51HK02Q96JRTXBAY2DS8ZEXQFXRM6SD43C6BJCBDRCSXXEXA1WG2FFG5CX65TZW94274CMHJNVAY6VR8Y8XKV928P7AHBK106R6TCTS164CG2001"
+ ),
+ noRevealIndex = null,
+ planchetsForGammas = listOf(
+ listOf(
+ RefreshPlanchetRecord(
+ blindingKey = "G17FNHNV8BA7BFZRG75PPKJSK7FC47GCXF8YKJZ491D115FQEGE0",
+ coinEv =
+ "85BB17VN2XV7TTNKV5MS64JVA7X39TMCSQQQKWFT30SZFJDA4A9F06SGP89GF7ZZBAVB8X3VTT94SKRQ192ENJS4K12715NKHVH11XWVKND063AK16ANGV2P3XCP32Y4GRJGZMZ068EQS623C4CW75VB2KPF45TKEJ3VX4M4GXWMB3H07F8NQDQGMPFCBDS8SG2HXESC8DXVE",
+ privateKey = "972E3RE0GFMMGXHZTYSFD0H3YDYGGBNH80F5GMHPCN7C8NQCH460",
+ publicKey = "BH1Y1VBKNH86RT9FD191MP82W7PAQCA0SN9GPBN23N02M9NBYS50"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "QR15AD5SFG20S9KW70P1MFAGPQC9WK7MSD0VFQXHKPDZTVZ6BZV0",
+ coinEv =
+ "DBKVPSHR1N8F04N1FQ04EPVBBWGETX5835M8Z9P7WF5X6BHBJW8KKNHZVR1JM9TRVZ9J0G5VDK7BQ9ESFKDCZD4PPB05K0MHW0HAJRKCCJ5PTNFXSRVB4V25D48XT031P0M53WDZRTPEK5G1XQ5KMQHZT06A70GE9N5C7SR4RDVA20AZDBBD1MKPR6NB2FMPAP2DR3NJENPWA",
+ privateKey = "AZXBY4QCFSH759DWTDCK1Q5D5JRNH3663JP2RTC3TSSTH3ARV0D0",
+ publicKey = "1C88APBTDQ8A2Q55N1CB3PD91PHYKJHY8QM2CYK0PGRJ0CTK4VPG"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "EP0XT6PDDJFKGFVG739QKNZCWRN05497TCBQERFRBJBB7W7Y2JV0",
+ coinEv =
+ "J8E5HCMEJZ78BP2VDCNA9FVFBDG9QT4A1FDBSAH6ZZJ2HNJNKYKS57ZVRPZ47B1B8J6WV77PXCDP7EZTX5YTJFPGGAT6W6QWYNGRTW1NPE454J0VPJ3DKT19EA10N27P6MTM35HZZ8GJ8CGW06HGRXSZ5HJA3SQFDRZS2XN68PYM28918ERVV9BDVKFBMRK9NDMWMM79EK198",
+ privateKey = "3V1SMYW447SGBHHZMZN8KDB21FYGNT0P2WK7A1XAR4AJYMXZNSGG",
+ publicKey = "SVM02H0CDHXQX2FPR2B31ZZKMT0SQWE2S3VEWSAZDEDVSDDBXW00"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "XSW3C40PHH03SX6G74QJ9CJCS6RFA2Q1V79G4ZS0J1F055ETW5TG",
+ coinEv =
+ "58FBPPVPMZEPMQB3RV4NJV7GA8GH8XYJ2NZ8N0NYG4M95434E1WT5GSGGM54X5X3FVTKKM5HRG28TCSD5ZCMC1V0KV6YTN1VEGQWVEN92M3TV761KWQDEJEZGHFKE7BMWP75KS37PV9SX4VABSEPS5VZCZB4C3RPG36QH2GH366G0AGQNX2F5T80SBS51Q3XA4FG44KJ14D32",
+ privateKey = "8SFAB3CA3JQ0KB18MREYVJYJYQ0BNYS1NCVPKHW7WS3YB4QN898G",
+ publicKey = "4QYR2AQT98J5QNMB44P3ZVFA0WVHJJTWG7WTXRKYJ11X1FYNYDGG"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "Y94ST132M2FTD579X8V9E3SK349CZQ5Z70RZ29VSP8S1PM08ZV30",
+ coinEv =
+ "HF7CMN9F3Q6EG85C10SQ313336MFQTWJ9ZQ85J4EKWX6Y2546ZNQCPMKDRM2E0KZWQFNR57SKE7EHJ4BWVT14582MT1YG0ZA7RC2DX5VGZSWVT3N568TNSN9TPKYD74KMAH2A0SSXY8QNXF9C3TQD0HPQ409FNV0QR4R65PJ0G4AJAJW3EH6W4CMEXRVXSD8DYT81H6KFPR12",
+ privateKey = "CTNN080C67WSNA96SY3ECCGYM017T3SYZXTJ3CTDEC23E1ZCXVSG",
+ publicKey = "R4G1DHWQCB8MS0E5ET8YTC9M9425R6Z146HKM1ADGQJ0Z2QA1YQ0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "2P1C6Z5RJNWPP2YW4SXGED45AM9H4EM7M063V8HKVSRHA4HGC5A0",
+ coinEv =
+ "3N07NHNBBSZMQP04EPSN3SC8DZXW0DHWGZ6QD7A20EWD8DDJ00PB2CH11KZP404ZXN752X4TMA6D09C459WXYQDBYQJ70J6920MJ63P9XRMKYHEKWRSJEEC3FX487VFMJJ8ZYYBR3C4YRVPADRXFPZ8T7YJ4667TJKCSRQJGS7V2DS953HQ509M9ZHHWRH19XBFPTET3R71ZT",
+ privateKey = "DWNV6AD7WKZMMPG342D1KB9PMH6D8RW2ZE5T0MB07KNA88NR2RF0",
+ publicKey = "21STR11NETSM4VVBZY65NEDNJHXMV27GM1NP2P47DJN4C8AQS21G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "XTRQX3ZCR6MAKY2AJR2KF9C4GJZ0W5HKK71Y0EHG683WBG6VFHQ0",
+ coinEv =
+ "867475JHJNY8BWHQH23CTCF3GEZZ2R1JBFKER7FTFQFNA9RCG4JR8Y89165HCW6GZQFBCWN8DQ91V8G3JH9JG4RPNJ6YZ6K1R2JNN4H99AR6DR96SDQ5N8YZSKFBFKD2QE8E9KAD8K05AVXPPQ4R4NDN45R7YTA346S1PB24GR0SCG7TPS1W59596KN3V1F4G17RGAKQA66T0",
+ privateKey = "T90M7DVZ78WVVDHBGTHW0Y38M88AWM48Q8MEP6WXXK4WGQQBF0C0",
+ publicKey = "K1MHGCPZSQ8HY8T8WJSAXE5DXH5SMXAGTZ8YZYFWE1ZYBYRHPG6G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "BM2NA21HG2A50GFXZQCQKK5Z23J0Y63SNM966M2FHDPM2XBYJND0",
+ coinEv =
+ "GAZ5VA6AD060FE466W5DHJK8E5TBY4MY6TEFD0TZ9BX45CT4S9V5NGV47G26ESW0Q31787GX1QB756J55BNQJY1SASN2942EARZTYCWC9MEA0MAGDFR6S5N6QRNFEACCHE37S4DB2DN4HWKEJP8JM8RADC73KHCBKBVKAEZGYAFD4P99BSER0S9S2ZH7GAWQYZ45YZY7W6C64",
+ privateKey = "GY4ZPR1BJB5YY0273H46GJTS4HXTWEFPZA4KT5E3Y9H12R8TQCN0",
+ publicKey = "9ZXVR8Y0TDT2H57M4XZ0HGG1R78RBSN3KZK2WAH678G09XP2WM6G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "X24KD0GD1GC8JWDBB72R94XJMD8FKR7T2GK1MX8DFJN0PY6EKH20",
+ coinEv =
+ "5GTW52KR7ACEGQ96SB92KMYBCC8XNPCGV9Q8ZS7MT1N27BH1NYM3BRTR8XJWZZ5FQ5KXEMT3FA374BQTZVPS9GTYHX9J3NMRSBJ6XJA6AGZDMW5VDXS3QWK49271WH1CSHMG2ZJ7XM0NJF86EZN7MPXKTV3WPD25AZ0EA4P980XB7DZ8GA04CVET8K0VFFP4KB13N4088KEB6",
+ privateKey = "K1W5XB7SESBYTAPP4PCS9DNYY4Z3HQ0464BW4T15VDZ64GFCXBZG",
+ publicKey = "W88CW2E3Y4Z0VZC8KE9Z4G6H2B8E4BCRGNDPVVD6MF8PNGRHGMH0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "CF2AE3C5HVQJSP0R7CBBTAYQK1NDTG5DTXGW6D6KKWG3053QMHX0",
+ coinEv =
+ "FWA6FJY0YM5HWA96QQNH2CGTTD0NR6DTN5YNRDZCBHDTE4A3ZJ5793175J6G0QRPTHNKEMPJFSQEVB4QH7MAHTPSXRJC28WEQ9Q9C7H3R5CDAE9SNMB61RXWV1D1JH195E13690RYZP62KC9349Q25JHADQ4Q4JQJE4MNJK19H3T1WHES285NHCP8BEE3MSE4TH567P1CBK58",
+ privateKey = "7ANNBTWEQPXP211W3TEHWTNRFG5ZFSCW393W0HG6N24PJN6XK6PG",
+ publicKey = "2DJ9HFE9HBQ0N8XNEH6BQD7R1H81BJD40Z94FVD016HEHT8JPCRG"
+ )
+ ),
+ listOf(
+ RefreshPlanchetRecord(
+ blindingKey = "J5979NKRJE2PS2NSVS4T4D6D1CSVJF7TBHKXA2FQYEZF6332PV2G",
+ coinEv =
+ "8S4CVYXJNHAQ93Z2ENGKJXNGYEXXMKE89KXH1TVRGVEY9PJ1NGFGENCKWZR1ZFC8W0TFXDGPYEAPHVPJAY0PT99G6GNJKDYV0YC89XGZK4F5HGN92BY7HT04AF2HY1RED8B0KKRKX9RV727XSV5QG9TZFBSAP4Y4HXZKEN1DEBRR2WJFYSZ2MYV50XPK5T7TPPMM30TX4PX88",
+ privateKey = "CJ6QMHX606JYZY9DS5VGPTFKDYYASAD60MFRBZTVEDQQRYX2KX2G",
+ publicKey = "WYYT5DM2QASGBCTSQKVXT5M7MREXV39MDF11HM5JX4NKMNXWY41G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "BJ9N8QGX1VYYRP1RR4WZGE2NMKY1EG3K6636HPY624MHT1FNK1T0",
+ coinEv =
+ "J5XPY46YX3S314FSBJD1MTZXCT87Y9PYKY17TJP5PCJNK2RF5JGMWBJVJQRXZCJCCFPY3FH4R7TD9A3FYB6JF4Y5QNM8Z46M90599H3HP2BSFZM4HW68R5DKPRE11GADR6BAW7ECFYAREDBZJNC5C18BMZWHP5XZ35CZZRWZ4GG1910KXX6A3789KW1028VBC46PHC3M61396",
+ privateKey = "FE0T9EZM72EZA5PX3TQPVNM9RM5NWJX4M9CEVFXW2BF2HF28AJ7G",
+ publicKey = "2Z07PKFFWXX53NXPX76HBF5RYRRM6E41A5KD5484VJ2DB7P64AA0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "4B9D3MG4TEHDCETMKBAC3XGYW7A4FFWQ8MEHT7V5SXC0BY0AF3AG",
+ coinEv =
+ "CX96G0RTN9N3QFYNKFFWG165XDKYW7FJEMB5K9SKKYB6A9DCK33T34VDDAJBH9VGG6ADXAM2DBVXZYPVKST9VK2KN943A9MRK0HNP8JVEJK84H9CVA4D1Y9N1351V05H2NC8TEDG43CS6RHSD37HX08N3Q03EW887GF2HF97MW23WH9CH2HBBNS9683DH7K5PY6H3MSQZE0S4",
+ privateKey = "ZS5031P7N3T3B52GNYGF7AEQ44HVCV63Y5Z0Q8E9XME6JC4VGQVG",
+ publicKey = "QB9HPVS5HXP0TVCACT7YB8B2Z7T9NZD47NA0XR49ZBCKJ03M7NXG"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "VTT1W5TK7JTTC6NJZJBQQGD6D005JKXXRGCWRG7AQ6Y86D0JPJHG",
+ coinEv =
+ "AGZ8ZKH7HJ2E4QBQVKH0SH94SPZFSQYG1YG0NFSE7T8HB7P6JKPF66ERSFGDH1CYRBJXA0WA1GFPX7WG528NBWXVEAN9HERZZ45XV2F6H4GPTT59F4TK27VK01NRXR93G2NYSS2V3FVVTNNTVB1BE57S5J18J2TD67RMYWWFCG0S1JPXMN196QBKBMB2KQNFP05G9WBPGH4ZT",
+ privateKey = "MXXTAHN3VWEPD3327H55S8F4JMZHBFFDDV9FBK4SQH8SR0TRRWG0",
+ publicKey = "XZ4VZMRZMZ32Q3DPPSPFHST8D41MX5YFT6BSF51C0D7M30M16NW0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "047V0KJ23HSGECQRR4MF8DRN3XT3HTHZ0QV585PBJ4HG75CYCPK0",
+ coinEv =
+ "DR1K7Y41WADGP405ZZTCPAWBS6QS9WZEW6VX3JGK1KHYNWZKREKA375TXZM73X558449PR805JCECYQNPCQHZ7HG7FNBT8DEDRRNP1JKJX0HPVTD155K1ZNVWWP2YY9THNG8729MKY3Q77CD7NN8MENJYH9E5RA4FXQYYEGQDDPG2YS0TM595HNA2KX1TD7KYYJ86AGT7FX7M",
+ privateKey = "C5M9VGNMQ2RM92DA92417HA5WGBTP2R2DPZWB6E5MA61S2TNX7W0",
+ publicKey = "JR43Q0EGAH85B2F1VQDGHZ33318FGB44158TC0ZB4YP8XF1N3KYG"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "B4DSQ4BZP3B1QPG21ME2R3BFE0DPX4GRSAVHF3S0V1YEG0JJX620",
+ coinEv =
+ "GAH89K67PPPNXZMC8Q7KEA9VHXDV1WB6WHSJXWB0852B4G5FECGAG9W36NV13AYMX7N37Y5X0NZ3WWKJHFKY8R328EEG528KPFV7E7NCJYZZB83PA6DDP5ZVAQF0N79WDANNEXRZEFT1H8CW0ZFK7533TR7FRTCDT9EWRVBDRJ23NX45C6CQVA6CK1JN9P0ZXP69FPJJAYHHE",
+ privateKey = "A0WS0Z3C9RGJNX9TX5QV4KYYVCTVNMW4N3FD10W97X6AF28KVB10",
+ publicKey = "B5MBRBBTF6NR53QPFJE5X8WX8GG9Z4TEDG2PCPPVNNB6XHHBNRF0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "ZTQHYH1RPX4XEJV6HG50EQZF6H6M74AY75N3DN4G81ENRS38AM40",
+ coinEv =
+ "FKH1C38XNYGYXK0RMMCVJHJAM60JA0M7T0XBVNV0K580M4VEW6SCR528BV65C7J3XYEZHCGMC1822J4AQXT5KS492KNB8G6MCJYJ95FMRD6R47PGWXSBH35JS95WSKE72FSB5W3Y12D6K50D5H0B7SKDQWAK4VZXNHC1S1X01KHV6KVZCTJW763K68ZABAD9R2TQYB686GE8W",
+ privateKey = "AQZAJRZV4R2FK1ASYEXNJ9E1K3QV0PHSBZNNPZGK2TR2QSBW4XPG",
+ publicKey = "N53YEANM052FMJZQDJGWGWERVB9X67VDRMA67X31Q7HKXFMJTJVG"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "GHJ6XF1XWYY623QDYNDXKTZ0PRSHAN41H8SHA4RGD1HCTV9PV570",
+ coinEv =
+ "3ZP6QZ4X6RH1R7C60T51P2HQEYZ4RMFG45MN47J6NQ6YC1M9M2BAFAQBM87JD8T1M69002E5QD2QZDV8HDJ492CT107TWZCE4TQ367GDSTPT6AAFS0JRSDJPBNME5KD6ZH8Q42KNMQ4PCM7GWA2ZQK7V35Z7BDJGWRFF65QGEPBC3F8PKVC92KJ8AN89ANY9H4YM55JFDCRSE",
+ privateKey = "T7BME1SM7Q6YEKNS6CCEGA7Q54SB1D451H1TJYYNGV6393R2QKGG",
+ publicKey = "QBTQNQKCGS6CH2W6AQSDSA7M1EFF0DC5Q1AE4MK2BQM0EBG3RJQ0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "1KFVREMW5KJZ3YS65MKCM40GNTCYBREPN7KM26YTVDW0GRPSDYTG",
+ coinEv =
+ "K08B7KDNK8C179M042GZ6BYGRNEK2M3YKTR25FZQ1H9AF9EG9GYFEP6Z2KZ3FH1SEPWYWHNQM1N3507PG99B8XWMR1ZG8BR1CFFM2EQFB5W8SVF145NS7SYXSDPMJ7A64NJZEQMZH0VS2XY262M3BDMVWP7FDEG3W1XN4V0S6GXA14GWRNKFG5MYX8RBPFC1ZHPNFHRGD1FT2",
+ privateKey = "0Q8W768BQYAPSQCD46R3GMH7QAVT6ZJ0T5YA2A3WXXGRGX7X2WGG",
+ publicKey = "AK1YSV2A9Y61E1XV313KXR1CJY81RZDKZW4TK9CZ82KK7YFJQDY0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "M2FEDXEYNBXQMMWYR36QY9TSVFYW910947RKFBN4AQ3GNZW7565G",
+ coinEv =
+ "3R65GCTT7EB685EGVXCJE928KPX1HH8YNHKD3JYYRJ26PD066GG21ZEE5HXSFX9D22NH31KMK4SPKB9QKNAWK539BYSP58ZZ9W011FYTGMKMDB8CMA354JMH03B814Y4MM64EE5BA1BDJ5BDZ0X6CT5BZ51QMD8NBZTKR1SE6YF1YZ6RJDQAG12E312F2VRZNYKXHGXNDSR1J",
+ privateKey = "V6XNMA56RBR8VRK0GG6Z76STRA6QMNSJZYB15SFS7XX1QPFNN6KG",
+ publicKey = "KM59YNG5ZZH9RHCN2XF4MY9YEGXC4A7HHN907FGG9C4W6ACFNY20"
+ )
+ ),
+ listOf(
+ RefreshPlanchetRecord(
+ blindingKey = "X84BZ3AZ0KMG3BYVRT3YT04ESCB7R3YEWVBZN5ZDDZ6WN00AEPR0",
+ coinEv =
+ "3B5ZHJ3NESVQ31QEBJXAGF6B5SJBEMXX5Q2FDTHRW807FDVQ3Y7T7KVQ5JSMB1DA3EQCQDNJYVZDQQQDHHHDR5TDEZZDJHX9S94KEKTRMNJDPFXSYABCCMWR7R4H1R7W6TM5Q4B5H2FYZXAVCAJJMC13ZK53N83SRRF8EVAJ2ZB51D6X7WPWQ1MQX3DPTXVZV4EGGPMYDP720",
+ privateKey = "FFZ1E4KGF8Z6HHYGQC1QR6RJFDQ3QRPCDM58NMK6YTFTSK6GTA9G",
+ publicKey = "Y7F33NG04MEFAK0QCBAHG325705B1326HCJ9WFXQCZVKMJJ8ZE30"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "E3QTYEC6NNZBQ6QTSX7TJ50CQGM3QCBABVPDSZANHFJNX8CW2Q90",
+ coinEv =
+ "6NE9SY9SZXTCJP4SW01T5R3YYZFCEG7AR5VJ4JNVQ9WPB7A1DGVXDZQ68233KSG5GEBV4W4T74CK5AC2MDKST7RCJVFSVP1B4EETSDVNWSJ8VF2V7BY8W9S85Z06WJE223PW692CEE4JA0Y83Z40Z69P28NS430BX9VMWRXMZMCH9FKB92TCVZGTXM3CJ2ZG5RRPNZB60VKKT",
+ privateKey = "ABAF6S5WZMZZ1ZWXJG7JVEQTMJB5ZBRR4T554Q0SNDZDRFG9MNMG",
+ publicKey = "HFVN1JANX4A4DSB4K458255GC5S879QTSBF2DDEM2K0HPZQPHF0G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "VRWF9B40EHKB4NZ6FXQG1P26XESD7FZXSEA9X53DZ1WVXS22GZAG",
+ coinEv =
+ "EWSVHPMDEBPW256A7K20TSW2T9EBQNKCHQWRJ1HJ85NW9K058H9C5C8QZ1R2ZEKDRYSEMPM920HXSE863BG8VKJHDR1ADQFREWH6TA2QA2ZANSMNJDHCA1EMG78A9PSMZCYE8Y82SY70MJSKZG9ZF0F5EG3KJ14X21RC6HY41P79NVETG4R11GF7MADSTF11G5CNB8EX26RN8",
+ privateKey = "DKHBJ9SRVEGAD7Q6K7PMH8DMA8WSRCK31PEAD6NWA4T6QRCMBW60",
+ publicKey = "DMVWT6WPYWHHX4G0H124HDQ6QDA1T447WQQW9NVR1ABJZ31E3YA0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "7HJN1EJK12M3JF7H6J6A82X9VB0VWCAC5MW4DA0W7524ZA620F3G",
+ coinEv =
+ "5YFPXNJ0SKNBJ6ATMD4JAPCMDFQD25RWYYQZXM7X6W8V9VBVBRT4M94MTMSE6W5H56XMCT5MF51MJVM9W090223R5VWTDQ9KEAHT45VPASHXNJ817R1A2S50F7TZTDHKE3ZKPJ1ZSTC9XDS57N0JGZ4AK3606PZ5211G8BDWSAGY8N0JYNASZRZ9JABN4HNTKRXRYZKN0V240",
+ privateKey = "AXZTP48HT58DB4BFKREZ34ARBVREC1BECK0KEYBWFWVP7K17ZQG0",
+ publicKey = "HP8PY0NPT0NV32RNMEYZ63YSDYB2T9J64KXAYV76M1N9Z4GPF8YG"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "KK0VT0E3KMHG39VT0B5NNTEXC9HMT9HJ6554THEVWHPHS96H4FS0",
+ coinEv =
+ "EMNAXG036TG4JNZ2TWBE688THQ1BZW8H5PCYMD4Z71K5FHSB6N8QE54V03RK0GJQVCDYPH699PPR5HDSWCBV13YG6WX9DX097VPMKTHSD7CYGH611RGDJ3KJXJQXQCDYXC869JZ9ZTBNGYGVZDVP16141Q32ARRF59T0MDKR6C4BJQ1V32Y2ZWHRD5QVRBAYS90KPDW6G00D6",
+ privateKey = "4NFWW24DP7V17ZN4D12NW6TENEMF54XZMNB4XVXM1Q31AMC8DKHG",
+ publicKey = "A7F9SY8413B3EQSVCES7AFDYRJH768J8AA8DVDR4BR06SHXT0W1G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "1KB9TCM2GDT61ARG99NA5TCG35H3P4K6ZGXEQA33P3XNQPSX33G0",
+ coinEv =
+ "1MRTJBESYQ1MSJC117JHRT90GS7WPCF5ACA198RGV4X0E464W2KMPFS4K43Z94NJ8QKP40YZCRX22ZR23MMCFWGNS5KYDV57JH4VZM6YSDVCRDGCBQDB87BN9TS2PXK8GXK3JA3G84X5HBGY7CX870B3MWBHW0V6FHMQNDXVFP1PCYZT0T0GAPPQ02MZ6Q8JGMSXT0QFXF9P4",
+ privateKey = "289K9AW8M3N45RZZNGEYBFHZEGCTKRY9TB15F5SNVAQ09BHKHEWG",
+ publicKey = "BZFET2N4MFYPQSMDASD7DJHA0EC8KGSB8RS3G34G8B5D6S9W1B90"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "B28XC87F006EE1B0GRMBEW3P88J324C7BYPS3SCVHAB3KRA7JBM0",
+ coinEv =
+ "8S771KDCPD9XFRXDT7938T8HX2XNGM7FH3WJGTYBD343GY5E427ZPD0QA4XAJYWN9038K0BH6Y0R2G42T8PCAM4W0YP31FPCABPZXPCXZP52ZDB6QR3ZG01KS9GBWAJ4PPKJG24MREBVTX37R90WA44G7Y8G5FCDBEQR1TZ3PZ0Q4C3K0Z3QP1HNNEH3WEP2QPFJD82YFF1G2",
+ privateKey = "592P3HWA2D245M1ZHA4AMSRK6Y5A9YFVP70VAKBAF6GSVVZE7MW0",
+ publicKey = "VS7N76T06X9GPEPGF1K7CD3ZTCJ4R8DJW715NRE30HT92JXQ076G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "9VQATS7SQ9QRM19602PN3QF16JEF9FM8GT1BZC8WRM9Y6HRV9HMG",
+ coinEv =
+ "CMZK656TCB6BRDRXVZ625SYZTGVKX56J77NE9B6T0EHRM8DRD7MR5WDRWQV8Z5WWDTAGKPCPA3XHCTGW5T156KK9RM340N53VPYJTDXH22CKY68PY8J4J40XG40YZW3F6TREE5TRFF51VJT7QV8CHW9PM67ZK2ZZWGVXV3KJF2HVDYFS2X8BHC19B266H5BK5EX6NJ65E9ADG",
+ privateKey = "4QVMP2V1FJQKTCTVXFBDCCR29T3BG40RCFZCJDPG6WPAS89EWWF0",
+ publicKey = "MV5RPV33Z2EFXSA6XPV0GVP2WBAP580JXAB2WD0G86FM29VM2X9G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "ZTKWFCA58WC7GPYDYBMAXCH12KPTRGTXR2WMYSX8HPSAZ4M47480",
+ coinEv =
+ "7Z9T151XYF58P4T8B7M3DS8VYFS7MXQPYG40V1JZ0SXXFNVQ1E6GX6C64F36FQADNPDB7S4YDNR80TGCACPXD5ZEP7JQ514Z1ZS43D739BBGPWH5QHGGDN91V2KDSDD8AS4V2M56CF26G0RBVKV8EZTM6ATQCQBVJE6Y4S98VP31WECYSW8RAYKHMHWKN5DHF1NMZMY86KA5E",
+ privateKey = "M87WJ5ECSYVVGK0PDTWPJCQS5ZE27D79RAZVGNDB8HWGC4R0X1G0",
+ publicKey = "7ANMTX5E74K8074Z50E36D8QX5J9TQ6B7WPN7XK3DPV4YFS1DX7G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "2522XN7JAFCVXSFMG031CSA20DFMY2MF5ZTZT6W3HP59RPW5S34G",
+ coinEv =
+ "5R0DF70BTV34K5X8DQG8Q1XY55RXCRQ8KF2WK12QP90DBY2QRWF5CJDE20X5BFWD5YKWVMKHV8QWE6SFGSHDBSJADYKA7WZXGBJ2EJPAYXEHMTTJ89KH60KZME1SDBBYZA239M98JV56ZAMZPNMG928Y4P27F5BPBSGQK3E3A700BDNW1TTJ5VYPXN8SNJANW74YFPJZN2HWY",
+ privateKey = "ZAX0KQPGATJT5EMWP7D6G4TB8GH6ZS6C17PJ91FS4F4VWX16H26G",
+ publicKey = "DNYGRGC1R41AH28FY063N71RDYPFYMDB20N491R8297R33G72FE0"
+ )
+ )
+ ),
+ transferPrivateKeys = listOf(
+ "KXR71TDT7A5GBZ6D92PWYC7PY9Q18KMVC4F4VRNYZ4NQ2V6DDY50",
+ "DWRWHG4NN81SWFYVJ1B1EKN9RSXRBJYV06Z8SQWWWTN6W6TRPCEG",
+ "3ZSZTTF3S1BG4MDW1GF8PE5EX2A2WD6WBTR8SYSD6GZBS4D4Z780"
+ ),
+ transferPublicKeys = listOf(
+ "2KQHHDF90PETJ852EX67W441PGZ5M47ZRD8XT0DY0HGY5Y1XWXSG",
+ "ANNVKXH143SB5EXDMS9XN2NXV0ETVA7KFJPS94Y6356H16K0JH5G",
+ "JB60PMEWBJ0Z51QGBHNHVQ51V8FT0C97QX64DFSK8SXEQNSYV1EG"
+ ),
+ amountRefreshOutput = Amount("TESTKUDOS", value = 1, fraction = 81000000),
+ amountRefreshInput = Amount("TESTKUDOS", value = 1, fraction = 95000000),
+ timestampCreated = Timestamp(1593697014952),
+ finishedTimestamp = null
+ ),
+ kappaKeys = listOf(
+ EcdheKeyPair(
+ publicKey = Base32Crockford.decode("2KQHHDF90PETJ852EX67W441PGZ5M47ZRD8XT0DY0HGY5Y1XWXSG"),
+ privateKey = Base32Crockford.decode("KXR71TDT7A5GBZ6D92PWYC7PY9Q18KMVC4F4VRNYZ4NQ2V6DDY50")
+ ),
+ EcdheKeyPair(
+ publicKey = Base32Crockford.decode("ANNVKXH143SB5EXDMS9XN2NXV0ETVA7KFJPS94Y6356H16K0JH5G"),
+ privateKey = Base32Crockford.decode("DWRWHG4NN81SWFYVJ1B1EKN9RSXRBJYV06Z8SQWWWTN6W6TRPCEG")
+ ),
+ EcdheKeyPair(
+ publicKey = Base32Crockford.decode("JB60PMEWBJ0Z51QGBHNHVQ51V8FT0C97QX64DFSK8SXEQNSYV1EG"),
+ privateKey = Base32Crockford.decode("3ZSZTTF3S1BG4MDW1GF8PE5EX2A2WD6WBTR8SYSD6GZBS4D4Z780")
+ )
+ )
+ )
+ )
+ for (v in vectors) testRefreshVector(v)
+ }
+
+ private fun testRefreshVector(v: RefreshVector) {
+ val record =
+ refresh.createRefreshSession("example.org", v.meltCoin, v.meltFee, v.newCoinDenominations, v.kappa) {
+ v.kappaKeys[it]
+ }
+ // use expected timestamp, so we don't need to mock system clock
+ assertTrue(v.refreshSessionRecord.timestampCreated.ms < record.timestampCreated.ms)
+ assertEquals(v.refreshSessionRecord, record.copy(timestampCreated = v.refreshSessionRecord.timestampCreated))
+ }
+
+ private class SignCoinLinkVector(
+ val oldCoinPrivateKey: String,
+ val newDenomHash: String,
+ val oldCoinPublicKey: String,
+ val transferPublicKey: String,
+ val coinEv: String,
+ val signature: String
+ )
+
+ @Test
+ fun testSignCoinLink() {
+ val vectors = listOf(
+ SignCoinLinkVector(
+ oldCoinPrivateKey = "884XH9DBQT9MA34YKCR7WPEP2HH3J1R9C3G7MSB8C3NHB0XEVZW0",
+ newDenomHash = "ZVZMM6GEFETH1S71BRKFKB03BTSTA8JZH26Q1PZED78NZ50EVVT0DMDBPDN5EBY90SK4AWT0J8CNPD7NB2TSCFMSKAS5V6DK26GEKT0",
+ oldCoinPublicKey = "3GSHK9JCTMEAZY7BMC1QQD28ABEJEJJJXHPVF2YXTET97WB81GNG",
+ transferPublicKey = "ANNVKXH143SB5EXDMS9XN2NXV0ETVA7KFJPS94Y6356H16K0JH5G",
+ coinEv = "8S4CVYXJNHAQ93Z2ENGKJXNGYEXXMKE89KXH1TVRGVEY9PJ1NGFGENCKWZR1ZFC8W0TFXDGPYEAPHVPJAY0PT99G6GNJKDYV0YC89XGZK4F5HGN92BY7HT04AF2HY1RED8B0KKRKX9RV727XSV5QG9TZFBSAP4Y4HXZKEN1DEBRR2WJFYSZ2MYV50XPK5T7TPPMM30TX4PX88",
+ signature = "H1V8D30JTVW8F7KERCWRDHPVVV6BZXNE0QG4V7ANEPTVFT2NKEWCCRM8NSG6D6MBA80ZSSYG1KYZXR8CWG0HV6N7QA48C4YEWK4M030"
+ ),
+ SignCoinLinkVector(
+ oldCoinPrivateKey = "884XH9DBQT9MA34YKCR7WPEP2HH3J1R9C3G7MSB8C3NHB0XEVZW0",
+ newDenomHash = "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ oldCoinPublicKey = "3GSHK9JCTMEAZY7BMC1QQD28ABEJEJJJXHPVF2YXTET97WB81GNG",
+ transferPublicKey = "ANNVKXH143SB5EXDMS9XN2NXV0ETVA7KFJPS94Y6356H16K0JH5G",
+ coinEv = "CX96G0RTN9N3QFYNKFFWG165XDKYW7FJEMB5K9SKKYB6A9DCK33T34VDDAJBH9VGG6ADXAM2DBVXZYPVKST9VK2KN943A9MRK0HNP8JVEJK84H9CVA4D1Y9N1351V05H2NC8TEDG43CS6RHSD37HX08N3Q03EW887GF2HF97MW23WH9CH2HBBNS9683DH7K5PY6H3MSQZE0S4",
+ signature = "90NEPFDDK99524R9RZ3H4NEK6S5N71CQN6DAXJ9S87YASHB0JYG3VJKSV7BVJVCRCHWDYYS61CXQ4XWW9DMS6HAV0PAPWEY7FPKB430"
+ ),
+ SignCoinLinkVector(
+ oldCoinPrivateKey = "8G7F1X5EQV9P7JK1YVY5MFYX18Z85FRQFCKFQ9J5HX8B03QEAR80",
+ newDenomHash = "HDYHXFJ65YSW7BEVCP1G282E4QHQ9KSF8KBQNM4JN4T3KD30YVN2G6G1HFWEMSHJGAW1VM6N8HDEYRXVYJ3A16C58WM6QATMENGJ5CR",
+ oldCoinPublicKey = "4670V0PA3JG928CJFQHGJAC3DKCPJR3HMRRYHKAHTEEGQDK78HAG",
+ transferPublicKey = "BH4PSJF3T0S9QNFARDXTBWXWT46AX5CH9JENP5D1HQVTNK9YXN7G",
+ coinEv = "PDGXW6PDZTFEGMYFCVW972V4WR9WXTYW6JBDCW52DCEPCECBK5AX8R276Q5JZ38ABQ0X0E8NSSV28H56CX3MR3YTXFAJKHY9M93EECWDCK6AA4259M6DYSS1D4AMQ2PQJH3Q0BFFXDS9X5Z6VKFM0EH05KM91SBB6679NC2SF0B4A1G2N5KTQEXCQADNJKHGGZJ704PBPYCGR",
+ signature = "7STWC1GZ6WWYBYWBNA1BDKFYAX8TCRGFP36A3W0TH72K51R9DXMVRA2H6N21CSA40Z3SQJ0NCFBNTR0781PX3027QX7KZ9KC7Q3SW20"
+ ),
+ SignCoinLinkVector(
+ oldCoinPrivateKey = "8G7F1X5EQV9P7JK1YVY5MFYX18Z85FRQFCKFQ9J5HX8B03QEAR80",
+ newDenomHash = "Y7N6C15W0ADR06Q2QKK15VWKV09FMHC0BHT0ZZ95FP8T7G7T2GCYWVMYRE605QCP7NQ02QYH3Y9V834G37K8KT7V3AY75N715AYX170",
+ oldCoinPublicKey = "4670V0PA3JG928CJFQHGJAC3DKCPJR3HMRRYHKAHTEEGQDK78HAG",
+ transferPublicKey = "BH4PSJF3T0S9QNFARDXTBWXWT46AX5CH9JENP5D1HQVTNK9YXN7G",
+ coinEv = "63X0T7DWHJHMKR7GX4D9CTNHVMGRKBN4C2VHBSNY6W8JMJYJNDD40XZXG9RB7V4W7HHJ605XK6R9VSBMJ8WCF5AQ3A0631F6MFTSEKTKJDMMP8006DGZ0XMW43JD0PD2XXX42HPAD6DY0ST941NYKNMA050ZMTV1JKNYW9495RVXQCMKPJ759KCC4BA6066E17Q7AVNS6EJD8",
+ signature = "5NVY7F9ZBSHY0JP1Q8JFTJYCY63QRT2JNV3R25050451ACYDXZ4YT0CE3TQYAECMVZ3F092GN7TQFNJF3G24JZN6T7XHDDG4MT0M82R"
+ )
+ )
+ for (v in vectors) testSignCoinLinkVector(v)
+ }
+
+ private fun testSignCoinLinkVector(v: SignCoinLinkVector) {
+ val signature =
+ refresh.signCoinLink(v.oldCoinPrivateKey, v.newDenomHash, v.oldCoinPublicKey, v.transferPublicKey, v.coinEv)
+ assertEquals(v.signature, signature)
+ }
+
+}
diff --git a/wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RsaBlindingTest.kt b/wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RsaBlindingTest.kt
new file mode 100644
index 0000000..0ced114
--- /dev/null
+++ b/wallet/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RsaBlindingTest.kt
@@ -0,0 +1,114 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Base32Crockford
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+// TODO move to commonTest once RsaBlinding is implemented everywhere
+class RsaBlindingTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+
+ private val vectors = listOf(
+ TestVector(
+ "TT1R28D79EJEJ9PC35AQS35CCG85DSXSZ508MV2HS2FN4ME6AHESZX5WP485R8A75KG53FN6F1YNW95008663TKAPWB81420VG17BY8",
+ "040000Y62RSDDKZXTE7GDVA302ZZR0DY224RSDT6WDWR1XGT8E3YG80XV6TMT3ZCNP8XC84W0N6MSZ0EF8S3YB1JJ2AXY9JQZW3MCA0CG38ER4YE2RY4Q2666DEZSNKT29V6CKZVCDHXSAKY8W6RPEKEQ5YSBYQK23MRK3CQTNNJXQFDKEMRHEC5Y6RDHAC5RJCV8JJ8BF18VPKZ2Q7BB14YN1HJ22H8EZGW0RDGG9YPEWA9183BHEQ651PP81J514TJ9K8DH23AJ50SZFNS429HQ390VRP5E4MQ7RK7ZJXXTSZAQSRTC0QF28P23PD37C17QFQB0BBC54MB8MDH7RW104STG6VN0J22P39JP4EXPVGK5D9AX5W869MDQ6SRD42ZYK5H20227Q8CCWSQ6C3132WP0F0H04002",
+ "7QD31RPJH0W306RJWBRG646Z2FTA1F89BKSXPDAG7YM0N5Z0B610",
+ "GA8PC6YH9VF5MW6P2DKTV0W0ZTQ24DZ9EAN5QH3SQXRH7SCZHFMM21ZY05F0BS7MFW8TSEP4SEB280BYP5ACHNQWGE10PCXDDMK7ECXJDPHJ224JBCV4KYNWG6NBR3SC9HK8FXVFX55GFBJFNQHNZGEB8DB0KN9MSVYFDXN45KPMSNY03FVX0JZ0R3YG9XQ8XVGB5SYZCF0QSHWH61MT0Q10CZD2V114BT64D3GD86EJ5S9WBMYG51SDN5CSKEJ734YAJ4HCEWW0RDN8GXA9ZMA18SKVW8T3TTBCPJRF2Y77JGQ08GF35SYGA2HWFV1HGVS8RCTER6GB9SZHRG4T7919H9C1KFAP50G2KSV6X42D6KNJANNSGKQH649TJ00YJQXPHPNFBSS198RY2C243D4B4W",
+ "5VW0MS5PRBA3W8TPATSTDA2YRFQM1Z7F2DWKQ8ATMZYYY768Q3STZ3HGNVYQ6JB5NKP80G5HGE58616FPA70SX9PTW7EN8EJ23E26FASBWZBP8E2RWQQ5E0F72B2PWRP5ZCA2J3AB3F6P86XK4PZYT64RF94MDGHY0GSDSSBH5YSFB3VM0KVXA52H2Y2G9S85AVCSD3BTMHQRF5BJJ8JE00T4GK70PSTVCGMRKRNA7DGW7GD2F35W55AXF7R2YJC8PAGNSJYWKC3PC75A5N8H69K299AK5PM3CDDHNS4BMRNGF7K49CR4ZBFRXDAWMB3X6T05Q4NKSG0F1KP5JA0XBMF2YJK7KEPRD1EWCHJE44T9YXBTK4W9CV77X7Z9P407ZC6YB3M2ARANZXHJKSM3XC33M",
+ "PFT6WQJGCM9DE6264DJS6RMG4XDMCDBJKZGSXAF3BEXWZ979Q13NETKK05S1YV91CX3Y034FSS86SSHZTTE8097RRESQP52EKFGTWJXKHZJEQJ49YHMBNQDHW4CFBJECNJSV2PMHWVGXV7HB84R6P0S3ES559HWQX01Q9MYDEGRNHKW87QR2BNSG951D5NQGAKEJ2SSJBE18S6WYAC24FAP8TT8ANECH5371J0DJY0YR0VWAFWVJDV8XQSFXWMJ80N3A80SPSHPYJY3WZZXW63WQ46WHYY56ZSNE5G1RZ5CR0XYV2ECKPM8R0FS58EV16WTRAM1ABBFVNAT3CAEFAZCWP3XHPVBQY5NZVTD5QS2Q8SKJQ2XB30E11CWDN9KTV5CBK4DN72EVG73F3W3BATAKHG"
+ ),
+ TestVector(
+ "0FDX7T3AXJ3AGTGMVSE8ZCDJGKH9DDBB0NHVF42S8D1Z4E2T1KAYF03DP6AA0GA6YS7Y64JR0YHFNWDGFSVH2VCTX57F71P92F6PCB0",
+ "040000YE5QYTJTCYF7YDWN2ECYAMBNENHQT7YT740XNC88V5F1K4YC2QD94WABBVHZY597B2BTGBD2NJJV028JKJTD1KBPKXF4D87B7ZJYZVQSA4ZB5H1FVPE7X7YQVG668YZ2YY229X29NM4B6QR0G3TH821QBT1J5EDPKS0RP8E6X4654DTAAYBEN14H96E8D1JFVE40773FVVPXXMX7ZXT7TCVC2EZFMZR1HQ2DDXD8KJZ9AEGS1YH4D629Y08T9X2533MS6R4X58VVKHN1YQVKJT2044A0S8B4AKCW2GJHMQM10XC3K7C3D1C841A6R96GRXPC02QVBQSA1D5VY5VG2T4HVC6NKVK5WAXDEYZNKYVPD9AV4MNCYGK23AZWGHX5E16BQTNG47C9DEETP2D87XFC9D04002",
+ "GJTG5KGE8XTSMVCMZBQGPFVBQ6RYX8RR2ST8JKNJKZ3KJN1SKJPG",
+ "2VN8NCT3JFWKDYJTR048YCKWHZ2DHKRSFA0Q42R8FB33YH2GMNH9M89AHRWSKCVYJ0000PE1PRWVT0DM95VQ7R883JZ2DEFQ38TVGMTKMSRBM6JK56A40S89HA6E3ZV40VZ9RCXGNY095DBB13SV68SYNGJ0MCDKW7PRA04YE8TRJYJV527Y1XJ4B5Y0FF8TSEE5YCNM4MW1M3P1AP6V8W6KKCAJ213N3P01K9WTKP45PGEJ0RWC0K5789V4BHMZAM2EFHPNEX24KWC7G59M160XF60ZNYCP9V0GV0V80K4XKD7CAXTS5YNZBPXQSTK90DEF750SHMVKATGS3GW1TGESAQ24AA4ZG5E9F58KWHBMD1WKP0VDBVTGPECQVE5F79DFD2WBB86M3NQ04RE7YGRWQ4",
+ "JFHDDJXQPAT0F1JF0GPVGC1CEQVQ9PXMY88KWKS16MEGPYBAMD5HAR2JHX8DAE8ER30ZYX1GBBYYKYN2M7Z0F29G6YHDF9SHZ7RK2XJW4J4HZPTS1ZV4TERC37Q3B1380XXP1SNM5DFZ0FW9W2PKSWEA0NHFYMCF8FR514WJ795W8ER9EKNGY51YSAF70ZG2DE3H0PPH69BWHJ4188R4AN7JT0T7WVVK0AQP9NAB36TAGMSRNKWEDAPMS0GP4M3BCY6WRXF8JQ52QSVWWMT03WZKRVGRRN03AY57X6TZD29AQB7QNKB4CGGPEW95Z2W2K7K3CV8EG1Z8Q3S7Y0M9VZ8D29CQQV4D75NQXRDFHMVZJ9X8EBPF1XHPM0FTK5GB7CDPRZ0SX5NF1HQKMHZMP35KSG",
+ "J99Q5G54YP6Y29HCR0X4385TQASSZDPMM0H3KZPD1744KRHJ895K4B41N4W03M9AN4RN5WR54NDRV6GAQ2R83K605NZCECD5BMV62Q7TWS4XVAK6ZG7MTHBNW9Y8Z5EJRR4BAJQ6QPKEPD3N0CXSHGPJM1KGEZQSYS4ZZWG7Z5YEYRAZ1S4VP03KEBJ1GR2PDRE61XBPCKEXXWWSWKWDP65WZKYYQNWVCRNZ4AQGR1N2H4EN3STBQC8126AFYCM73K9VHCB7P171E8AP0QZSKP5SEK8XCA3F16KYBZ0MD7EMY1NB0KFF3K51C227Z2WMCW8DWSJM39CDP7B5E7ZZATVQ17W4FN2HT00B26QH4HPCJ0Q66JNF8T3086ZEAKKTK835RN7SMGZATD8NN2G7QGW344"
+ ),
+ TestVector(
+ "E1ECK6HSEYXRDPGC9ESVVBFR5516FWGEXW0K3K7PDGPHFE32RB6F5G0EM5VHBVGW4TJPTMM314M2YTZQ1RJDNJGBTC2QP8TQTB04AYR",
+ "040000ZVQC32P71MYD6A4FTT4P2DABQ100S7AEQ2BB4CW1YY8RMXK24SE7RG15VQT88KY0JP8JGTDTEA2ZE8MBHNGR9TQJXHAAZQ67D54826S5ZVNBNR4ZTC6DKF2KTQEGFBV80ZNXWA97SV2G8DXVSER8J1ANWADDMCE2C3QKX7VJVP042V48DKHV36XCS2W5Y579CKY7E4MXPK7CCP1B3FPXE86JRZ3FBVHW0WBXSJ45HQYFNTFA8GEWNN81JDVX9D36XCX70C5CCFDMY53J6ZYYM6MSVTXFM4X33TMCX21EZARHCS6Q9SJV201SPQPXZ876EJ0CPYHC9PYRZTWJDHECJYMS97CFHEWCVR7C3G47VZPHJS50YXS2CP8RT8CSPG63GNAFBMM7Y67BMTED0HND0BDX4Q04002",
+ "XN5QNPBBPD69EM597VT7W1RCJT8N1DC4PS65CTV3V9KQW6K27VB0",
+ "7YC9YVSJAPJ5SMQ68W761K61BZM1J6NMQ5VW57SJCVRQ7G1KPCZ39F79BTY96AP134SES5QM3AQSMNA7FR9C65B1NA2BD7EKM12SNNSG25FRQRD902692Z535N2ZC6R7BQHHQJVGSXDA19CH2C7YWSWGVR04DBA3RPAB2FWRV8D022Q2W4RMFKV8ZETWF1QZ74Q1YJFJW7SBGFAM40FRW881WFQGX1N0HT63FJHH5B9PSGQARXXSRHJDHKAM1QW1PA2M5DCGP0875HE1ZG90RPMVXC33BP7251SP3XJRX6RNFGVTFEXP2GDKP223NHCKQ09E0P4KT5C2TJZ0TAVRDKDJFERBVDQJWSQWNXHMHJK210N2ERZAAMAFSDBGEMAR07T2Y9YJY1DX9QKWXHSTG8AGVG",
+ "RK6Q1F01FGNFY1EA6M3T9879HD8E84CP30G8PQQBED4VWF3AZMK0GFZR7A2Y2VGSSVJP4TCB6FVSV9E810NN2Z5SP72MYY9EHEGEQGRD9840R81A3HD3Y4BSJD94J20S18TNDFVDXRQMMJ8KJGWZRRWPCRHSYPFYDJT6MW4DAMMNQ1437BS1YP6AFR9RDW8AFRV0M1E54EAB8BY3S9W7E80RJZT19W4XFBC777X3VKFPP8HNVTFM4970V8GYRFZBAWQNPD25V72H40GK9CZK818XHJWZNC0Q86XCNFZ05GEW1A4AQ8921A7MTKSBKKNQ8ZRH9PRJ7TZTZ5AF9RHGY1EXKC0KG4PS74ACXCRQV316K64WEXP00FF8455Y1WB5J8V4894Q93T1A8W8TT8KRKA6RG",
+ "9AWAWCGCEN0PPGJ5NRPYVF4H1R845DGX5H26JQ8TAAJBYQNGPMWQR227M3T7RQJHTEXSSQZR95X5PDKNFQ57X72JJ39Z0WQ738ZJY1ZTVWNKBNW6S3CM9WMVWSEZD84W8EXVPD0A3TC6ACM7BNBV96N33F2SD6BC8C9QGW7Z5RDR2KXDZHSD16C5ZEX6P3F9Z0K6DVVAX247CZZ8Y5VVQQANGJ99TN0G7ZYMJBH3WQAFPGN40FGXC56ZBB8WZ0Q9Q1T1AK2VJ45VTJTEG78GPA2PVSK636MPY3F1W496BCVMVD6H4GHG0RWJR707EPTE72C6JJV7VT8NXH0G6K6KF6THNTEMK7HDZYR1S896S36H9SV3XWQ55NXQCE7NQYFV2WRA2MJ02V6KQ5D78ZJ2PJ5WSR"
+ ),
+ TestVector(
+ "7953QJ86Z03P607D6PZH39569VYQ9TVZ2NNT7CQJGA5P15JVBSRDC633YSSE5M4W6XWG1X4MBFEPYN7S1T9BECBGEGE1ZZK6789JQ48",
+ "040000Y7X1S35KW1MTPR19MD8F59QVY42WV37WB8YFVA9Z7SQ1RFEPF4HF3NN1XSSW1XMJS8DYJK57RH8KKQC5EHR7DS7PKWCJT6RJRZGAX0GN60990HB3XKM34PSKW4ZTAZDGS1KSND3Y9TS50A5G7HDF4K27SC8FXNV2VGGW9YQ6CC0F9E3K6D3BD1S39VC1WTPCYP62BVT2B5H9E5G49TVPDYXRTPDSK7FSFYGMTTYBVVJXQVSC2QKS3ZBY2HF1Z29AEV2247KVP8PP8DAB9J6G6WEN1HCXCCSNP5WK8QPJQAA9QWW1G4Z0P7Z7BGHFJFPJHY1AHQ5GGSSDPHET2SMP6GXJK2SZ42AV4CR85YEPHKVRMAHN8ATSK89BWAFNZKWBZ0P31NPY4CW9456MEDDZT1SDZB04002",
+ "KRVDHJ0V57V8JFYRG1YRDB5Z5A5YM10QA4ZG2SH68XNJ2V71PF10",
+ "B62RQP191DY7BZEEWVPDPE5S5CQMNFH3PBBY927F70W6JF5S8SJ3NGM6JV612SPJWRX8XM8PSW691R69QFGSQ4W54KT828TG6AHG1XS3T74HB4S4XA1SVB7WB8W7TR7ANKRHZ3EWKC52FKYRS05SYRYB9QDZ072D5S4WHNG3YATSF9AD9YA0S5482S5VY0C0JFET733CVAMR0Q9SC4BWBPDJSNH8WK3ZDFYM4DZJNFPHRKM253TW3QF1WCHDQVJAPBDT3J8PHEQGAKVJWJS3WAPRFW2RY87WF0ME2YY75H91ZCP0Z0KYYWF1GQC7T5BP6E1FWJH73P6QNFA8H9AX86EBGK7WMVMM0T9PDPAVHW0TJB7FSX5JN37R5EW3FCR9DGYE3NY40ESDYP4NMTEAW2YR04",
+ "79DRCP9VWHYMG4B110MNKZTC5EJM2HG5DRY1WVTZHRV0T2P9YW48ZX3HRSSW3CSC9HH3J8AN3XD06KTYWY29HFV61VFRC2CST20H8RGJ6QNWMTSYB6636C52PENV318QM9C2FQXVTGS8RFX9A3AG4DCRW3SZPPWPAG3GHV8VC3AJMT5ZA39E0B8QDRXSMAN1CA9ZF7NVRKZ7ZBY91XDYGSYGZXA3YFF94EGF62HPJWX2QBN6A8M542EDS011VF9ESQ2H4WEPXTQPBEGF00MRSV0T44YXR3R9QHY607EJKJF7VFDXP0TAZDD5Q3YK2J5GFR1CH6Z2QQ6RN8T2X8GP0PDF722PBYNNCS70J9VRK6VW2W1M0MX8VF28NMVWQ17F45HC4YDZSK0S444T505VMW2BPG",
+ "EQZKF4GGRF7M2RAS80XPP2KEXQT0SC55NEQ16X5MYJT0QDM8ERGVAGNT8F5NARG944VTMJYDD5WQ3QPFPMFB25SJ8VQ41MS3X3C7SQJP060JHDCK2297XKPQ1YN4PXQG55XZCT2GQHBX6Z1A10H24ZNPACRB0YPAE1RTVDGST0SQ5C8KS1R6SHC2NR6YGPS41ZZYMP48FBQ5KP0N5ANGKTBTMXM1G6CH8J9ARS7283WF54Q04XK9Y79HYW58RRFJSTQNTGHSCSV0KHS245VASSVG752F2XYVK96KH4715G7F4PHAWVW2F85XTZM15FPNDR3R34NX15GYVW2ZT2SXNNAVA095H60BY1G5J977XE5TPTZ4M7T9MJZJG179Z53EPDSMH0VRHKTVK0T44T5BXF1H6M"
+ ),
+ TestVector(
+ "8H0BJPDY2PFSKW9VQE4HQ8W5E62M4YK3169DE7JFNRC28P9G76HDFZMS16CMTWDKRNC4JVHXAH1J03992XDW77N4EGBR937AA5VCJ1R",
+ "040000X1ETZJWD59MT15XTV37NB28BM76G9G5KCHWRE9ZAR1XJW1RBPCTGNB9TE36FAXQV8FJ82J390PHZPMR45X3SRC9CRC2CK7WQSPXW79826SDX8CASC5J2Z92D8ZNN38SEVK3F0PE130TDQ28B9K45FEXF7GEF0Q2BJFCAJ8B6QVGD5NEDGKVCH6DVP2K8PAXCHMSBHMGH9MPACAHZX502VPEHZ6E1ESBNS1RX5XRJ1GAVH5VD2TTFYGRBD6VGSR8G8B9147Y1YB6FJKGTFNVCACC29NFEW98EDBVRRNXEX35DXTAV1HV5NFSA4EN7M7AE8XMT07TEF0KHGA9S0H8NHHE90E0TCAPSYKFTBAMR5YES6KXNNSJXN3SPS9BDC9FTS8K5W3FDGPWVDN6PNS4VN4DYEK04002",
+ "DKQYT4AT58TD2WY6AF7X8TG30ZF0V6WZ4BZ92Q68ZJQBKVJ3JDEG",
+ "HKSKCY5F1PSPCHMDWS41B7RF1ACS4ZJ882Z73GTC8TT7P2M8QP68CAC8SWJ972FN3XHH4JYZDTW565062Q48ZTR9ZG68RHWAMZ689RFZ9Z4N4P8XMWHXNZCCQCNNZBRZNYHS33GMFXV1JJJD1VB8PJSFYFQCTGDDMFM54JJCQZYM0E93VJMDNWR02EQTWMAGSNKERJDFBQ2HEE07TS6T8AW613J0RQBFB0XD0EWMP10CCQP26RD2Q9HKVZ9EWYV7DQ3N8S1VBPZNTKB8PQBNJRTMGA3P2EVZT6ZD45V6KX4DG86T0EZGX4JWDS3BW47VZC1N0P6N72A4NKHT67DJEN0V8YPJNGRQG80DD92K4HPYK6JBX96YRJ65WKP3M1YQZ5WP325DF4FQS87R1AKYNKNR9R",
+ "BD9TNRSSMEG2NTSN8E7G04VMXKNR4KZK6CJ31TX9QDWBC7T86FYJR3JSYWF1KPNDQSQWF896JV5TW4YY4Q32845KH39DXXTGXMFHBCDC32WEFR65YGPP45A2Q6SVM1WCNJF2RPSTQZR072YA4JR5NF8CQVGPNB459JMFJSGZ2ETBAYH1V65DJ20VBHRW8E8D6JBZQW6MD96C1D7RNHAYEDPGCRPS6RAR8HR0M08E2HXGSBD1PB2C6VYH2KQQBGDE6XRPX0NXD07E5PG46J7X5EMJ6RK8MVVR1XQVKSGWJ2T9RZTRT957295016DMW9G1814JENJ7BVG9954ND37GZ9H9DQXA9GY4QPB778Q2N66HGGSMEZYCHVTZX2FXC25PCCY2XNH3EXSA2742EXE5PJG0QC",
+ "3WQ1XV9PZGKHSFJ1AQ47RZ3NE8M8ECQ1RA0ZNSG6WJJYVV9F2SENEDXWN5M3DZR3F5WEGAB6VVT5V7EBW42H5A48D29AKZQF6SX2ZEJNQE1DCRBTCC6EPJGBJFXRN8AK4PT8JDTP2VRAMRFN62P2VMHZS4Q5EH431E7DYCPPC5YC5CKJT26R22Q1GZMW972RQ5WEYDMC7YW7M5WQ22G2KACR3MYEJ1BYRPD981ZPFKEB4M8TJZ1N2H7NM3S52PMXZ5HDSEMA8Y2MWW3KG3YJF8P0K8ZWG409S60MWVA7VEC1P1PREKT27S8CCPDQRFR5W9S6R71FK1KARJRCW5GPZCESFQNZT3A6DT2MNTWHG7KBPYAEZG8ZDS5CBFVRK7BQ7ZHVQDB7TTT2M64RF52DX7BNW4"
+ ),
+ TestVector(
+ "JZB0QJS6FC7K18RTF3T2T4XVHNR98BVGP811NE25T1333P7XQQKAKAMSCQMH25D0H0JM8ZW5QBX6H1SX6SZMY7VYAY2HEKRX9JPV86G",
+ "040000XVGVWCHVQVTQ06Q5V0XRAVQKPPZQZ68GYVXSC5RAG37VDCG0CEQHS4876BX6DDABB2WFY7TRJ7MFKTMMDF7A7ZW9PKQ8S3RQ15TVTKWBFGGKBKYSP6CVHNG9AY738NCPC8AFWYGP8J2VJE9HRR7M1GQK19E2M7Q2Y54KCSZ583BTNX275DW6EYYE1KBV4FK009Z621EHF5R87S6VQDSBCKSK15JCH1JYC2VPRHHAEGRA2WYX1HD9KFET0C9G1CZJB1MHZ5Z7Y803YZJH441P3PJJTRB9WCTA03H6M43CJ9MB33BEJ3KR22R8CS0D6QC2E7ZQS5MGBWCF51FK97SHCJW93SAT7VHB3YX5VVDNTW9N3SDW56HNWT11D306H9VN7BTP84T404VF482Y09K4SHEF5704002",
+ "RF9NJTZVPAFGRZ062MQBV1136KZQDGVQANQTRD44Y6QTX3Q47BN0",
+ "DVW867P5XYQAX2B4V9P1N0F6W24MB1E5F62XQS8FBXT3RQBKJYMXSY6P48SEBNMQC3WPXK4PR1QDEYT24B4TVGRN9E4RYWAAE1VNHTBME20HTK6YZBH9EX91WW3Q3XNCARN5M01YKPV3EKRJM4N7DCN47H75WK6QJRQHARC00GSE1640H5BJV9X1DBY73307JT36G08E46RZ48E6HNYPRDVW0KMGJDJN3YZ93ZBRX5B4S5YE89ZSW6VCRXH0X0H9ZM1G67ZXD12CXAWFV1M3Z8WYMY94Z351T3WBFTFPB11XWE1H1H7F8QTBJRZNGBGKHA4X0X8BW7QSB1FP9F1HQBPN2TPW9VN167GEX98MD8C1CSNE9G0GGY60MV74Y12JCTCKWGPMTWG95KZQY5VYVPATXW",
+ "64DS6GCWVDHC1032251MY50QZ1XVVZHQTH318Z51B3A3PDASXNNWA1MX52GS9V82K53WDGHTSDAJKPTQPC1VDT6VFGFP6JAANJXVDXJAFB3D30ZMRQGEN459B876Q7C8072NA8T3JVMNBH12NHE03GT9RQPESCKR79GAYDVYKW56VJ6Y7R6A4VG9YGFWZWH6CQ4E5DE49FWYMQ815DQHKN0AR3FMK006VAN1PRTNS0SRX67H3BQKSQ3K0HKYJSM96PZZFP8H5WNCF4WFMQP5JDVWH0WZGFEMFJE4BPNXJ0GPPJ0AQA5V8YRT0QY7DK5AVFE7DK0W6VVYAPMNVQDP2ER4ZD56HZVP6AQWCCMHCMJF39FTY7JT2CY14TPYP75RJB9SACVVMP7XDXXBEEJDTBWQ38",
+ "BCDNEWHSBMDGGEBN1GD85C7T7MCPG3AVB7V8EYSQQ6Q5JNWR95VXMBSQ1QQBAEZ9B0H9VPCZW3G1JDR68P56ZFDDZ4CVWEAK9AWMR5B2T2VF8PD7TP1ZG0YN02GJN3J20NJ5TV06FSZX6JF8MFWZRP646452VE1PY18KCH6CDEHA9YMJREFXV5HMV8KNCHFC0RFZ0CE0HQKF6PZ5VRWMY7ZD3ZC9SGNMWDRS4CB32W9WHX83ETD8ZMSPQENHKG833QY9EPZZFX520D6GKEBNRHBBJS0AGXWKM0DC6V2CE7Z4PR6PT8YT41JP2RQ8JJABSTTFWEE7TR17YE9GG0FSMHAXY7CC3ZEMJRZYKKYQ5SZNC2GHFS5XN1D18TQRASJSWRKSEJBSQ79QDA0GHFKG3D6XT8"
+ )
+ )
+
+ @Test
+ fun testBlinding() {
+ for (v in vectors) {
+ val bm = crypto.rsaBlind(
+ Base32Crockford.decode(v.messageHash),
+ Base32Crockford.decode(v.bks),
+ Base32Crockford.decode(v.rsaPublicKey)
+ )
+ assertEquals(v.bm, Base32Crockford.encode(bm))
+ val sig = crypto.rsaUnblind(
+ Base32Crockford.decode(v.bs),
+ Base32Crockford.decode(v.rsaPublicKey),
+ Base32Crockford.decode(v.bks)
+ )
+ assertEquals(v.sig, Base32Crockford.encode(sig))
+ assertTrue(
+ crypto.rsaVerify(
+ Base32Crockford.decode(v.messageHash),
+ Base32Crockford.decode(v.sig),
+ Base32Crockford.decode(v.rsaPublicKey)
+ )
+ )
+ }
+ }
+
+ private class TestVector(
+ val messageHash: String,
+ val rsaPublicKey: String,
+ val bks: String,
+ val bm: String,
+ val bs: String,
+ val sig: String
+ )
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Amount.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Amount.kt
new file mode 100644
index 0000000..2d39bb3
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Amount.kt
@@ -0,0 +1,210 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import kotlinx.serialization.Decoder
+import kotlinx.serialization.Encoder
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Serializer
+import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
+import kotlin.math.floor
+import kotlin.math.pow
+import kotlin.math.roundToInt
+
+class AmountParserException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause)
+class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause)
+
+@Serializable
+data class Amount(
+ /**
+ * name of the currency using either a three-character ISO 4217 currency code,
+ * or a regional currency identifier starting with a "*" followed by at most 10 characters.
+ * ISO 4217 exponents in the name are not supported,
+ * although the "fraction" is corresponds to an ISO 4217 exponent of 6.
+ */
+ val currency: String,
+
+ /**
+ * The integer part may be at most 2^52.
+ * Note that "1" here would correspond to 1 EUR or 1 USD, depending on currency, not 1 cent.
+ */
+ val value: Long,
+
+ /**
+ * Unsigned 32 bit fractional value to be added to value representing
+ * an additional currency fraction, in units of one hundred millionth (1e-8)
+ * of the base currency value. For example, a fraction
+ * of 50_000_000 would correspond to 50 cents.
+ */
+ val fraction: Int
+) : Comparable<Amount> {
+
+ @Serializer(forClass = Amount::class)
+ companion object : KSerializer<Amount> {
+
+ private const val FRACTIONAL_BASE: Int = 100000000 // 1e8
+
+ @Suppress("unused")
+ private val REGEX = Regex("""^[-_*A-Za-z0-9]{1,12}:([0-9]+)\.?([0-9]+)?$""")
+ private val REGEX_CURRENCY = Regex("""^[-_*A-Za-z0-9]{1,12}$""")
+ internal val MAX_VALUE = 2.0.pow(52).toLong()
+ private const val MAX_FRACTION_LENGTH = 8
+ internal const val MAX_FRACTION = 99_999_999
+
+ fun zero(currency: String): Amount {
+ return Amount(checkCurrency(currency), 0, 0)
+ }
+
+ fun fromJSONString(str: String): Amount {
+ val split = str.split(":")
+ if (split.size != 2) throw AmountParserException("Invalid Amount Format")
+ return fromString(split[0], split[1])
+ }
+
+ fun fromString(currency: String, str: String): Amount {
+ // value
+ val valueSplit = str.split(".")
+ val value = checkValue(valueSplit[0].toLongOrNull())
+ // fraction
+ val fraction: Int = if (valueSplit.size > 1) {
+ val fractionStr = valueSplit[1]
+ if (fractionStr.length > MAX_FRACTION_LENGTH)
+ throw AmountParserException("Fraction $fractionStr too long")
+ val fraction = "0.$fractionStr".toDoubleOrNull()
+ ?.times(FRACTIONAL_BASE)
+ ?.roundToInt()
+ checkFraction(fraction)
+ } else 0
+ return Amount(checkCurrency(currency), value, fraction)
+ }
+
+ fun min(currency: String): Amount = Amount(currency, 0, 1)
+ fun max(currency: String): Amount = Amount(currency, MAX_VALUE, MAX_FRACTION)
+
+// fun fromJsonObject(json: JSONObject): Amount {
+// val currency = checkCurrency(json.optString("currency"))
+// val value = checkValue(json.optString("value").toLongOrNull())
+// val fraction = checkFraction(json.optString("fraction").toIntOrNull())
+// return Amount(currency, value, fraction)
+// }
+
+ private fun checkCurrency(currency: String): String {
+ if (!REGEX_CURRENCY.matches(currency))
+ throw AmountParserException("Invalid currency: $currency")
+ return currency
+ }
+
+ private fun checkValue(value: Long?): Long {
+ if (value == null || value > MAX_VALUE)
+ throw AmountParserException("Value $value greater than $MAX_VALUE")
+ return value
+ }
+
+ private fun checkFraction(fraction: Int?): Int {
+ if (fraction == null || fraction > MAX_FRACTION)
+ throw AmountParserException("Fraction $fraction greater than $MAX_FRACTION")
+ return fraction
+ }
+
+ override fun serialize(encoder: Encoder, value: Amount) {
+ encoder.encodeString(value.toJSONString())
+ }
+
+ override fun deserialize(decoder: Decoder): Amount {
+ return fromJSONString(decoder.decodeString())
+ }
+ }
+
+ val amountStr: String
+ get() = if (fraction == 0) "$value" else {
+ var f = fraction
+ var fractionStr = ""
+ while (f > 0) {
+ fractionStr += f / (FRACTIONAL_BASE / 10)
+ f = (f * 10) % FRACTIONAL_BASE
+ }
+ "$value.$fractionStr"
+ }
+
+ operator fun plus(other: Amount): Amount {
+ check(currency == other.currency) { "Can only subtract from same currency" }
+ val resultValue = value + other.value + floor((fraction + other.fraction).toDouble() / FRACTIONAL_BASE).toLong()
+ if (resultValue > MAX_VALUE)
+ throw AmountOverflowException()
+ val resultFraction = (fraction + other.fraction) % FRACTIONAL_BASE
+ return Amount(currency, resultValue, resultFraction)
+ }
+
+ operator fun times(factor: Int): Amount {
+ // TODO consider replacing with a faster implementation
+ if (factor == 0) return zero(currency)
+ var result = this
+ for (i in 1 until factor) result += this
+ return result
+ }
+
+ operator fun minus(other: Amount): Amount {
+ check(currency == other.currency) { "Can only subtract from same currency" }
+ var resultValue = value
+ var resultFraction = fraction
+ if (resultFraction < other.fraction) {
+ if (resultValue < 1L)
+ throw AmountOverflowException()
+ resultValue--
+ resultFraction += FRACTIONAL_BASE
+ }
+ check(resultFraction >= other.fraction)
+ resultFraction -= other.fraction
+ if (resultValue < other.value)
+ throw AmountOverflowException()
+ resultValue -= other.value
+ return Amount(currency, resultValue, resultFraction)
+ }
+
+ fun isZero(): Boolean {
+ return value == 0L && fraction == 0
+ }
+
+ fun toJSONString(): String {
+ return "$currency:$amountStr"
+ }
+
+ fun toByteArray() = ByteArray(8 + 4 + 12).apply {
+ value.toByteArray().copyInto(this, 0, 0, 8)
+ fraction.toByteArray().copyInto(this, 8, 0, 4)
+ currency.encodeToByteArray().copyInto(this, 12)
+ }
+
+ override fun toString(): String {
+ return "$amountStr $currency"
+ }
+
+ override fun compareTo(other: Amount): Int {
+ check(currency == other.currency) { "Can only compare amounts with the same currency" }
+ when {
+ value == other.value -> {
+ if (fraction < other.fraction) return -1
+ if (fraction > other.fraction) return 1
+ return 0
+ }
+ value < other.value -> return -1
+ else -> return 1
+ }
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Base32Crockford.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Base32Crockford.kt
new file mode 100644
index 0000000..9043731
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Base32Crockford.kt
@@ -0,0 +1,129 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+
+class EncodingException : Exception("Invalid encoding")
+
+
+object Base32Crockford {
+
+ private fun ByteArray.getIntAt(index: Int): Int {
+ val x = this[index].toInt()
+ return if (x >= 0) x else (x + 256)
+ }
+
+ private var encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
+
+ fun encode(data: ByteArray): String {
+ val sb = StringBuilder()
+ val size = data.size
+ var bitBuf = 0
+ var numBits = 0
+ var pos = 0
+ while (pos < size || numBits > 0) {
+ if (pos < size && numBits < 5) {
+ val d = data.getIntAt(pos++)
+ bitBuf = (bitBuf shl 8) or d
+ numBits += 8
+ }
+ if (numBits < 5) {
+ // zero-padding
+ bitBuf = bitBuf shl (5 - numBits)
+ numBits = 5
+ }
+ val v = bitBuf.ushr(numBits - 5) and 31
+ sb.append(encTable[v])
+ numBits -= 5
+ }
+ return sb.toString()
+ }
+
+ fun decode(encoded: String): ByteArray {
+ val size = encoded.length
+ var bitpos = 0
+ var bitbuf = 0
+ var readPosition = 0
+ var writePosition = 0
+ val out = ByteArray(calculateDecodedDataLength(size))
+
+ while (readPosition < size || bitpos > 0) {
+ //println("at position $readPosition with bitpos $bitpos")
+ if (readPosition < size) {
+ val v = getValue(encoded[readPosition++])
+ bitbuf = (bitbuf shl 5) or v
+ bitpos += 5
+ }
+ while (bitpos >= 8) {
+ val d = (bitbuf ushr (bitpos - 8)) and 0xFF
+ out[writePosition] = d.toByte()
+ writePosition++
+ bitpos -= 8
+ }
+ if (readPosition == size && bitpos > 0) {
+ bitbuf = (bitbuf shl (8 - bitpos)) and 0xFF
+ bitpos = if (bitbuf == 0) 0 else 8
+ }
+ }
+ return out
+ }
+
+ private fun getValue(chr: Char): Int {
+ var a = chr
+ when (a) {
+ 'O', 'o' -> a = '0'
+ 'i', 'I', 'l', 'L' -> a = '1'
+ 'u', 'U' -> a = 'V'
+ }
+ if (a in '0'..'9')
+ return a - '0'
+ if (a in 'a'..'z')
+ a = a.toUpperCase()
+ var dec = 0
+ if (a in 'A'..'Z') {
+ if ('I' < a) dec++
+ if ('L' < a) dec++
+ if ('O' < a) dec++
+ if ('U' < a) dec++
+ return a - 'A' + 10 - dec
+ }
+ throw EncodingException()
+ }
+
+ /**
+ * Compute the length of the resulting string when encoding data of the given size
+ * in bytes.
+ *
+ * @param dataSize size of the data to encode in bytes
+ * @return size of the string that would result from encoding
+ */
+ private fun calculateEncodedStringLength(dataSize: Int): Int {
+ return (dataSize * 8 + 4) / 5
+ }
+
+ /**
+ * Compute the length of the resulting data in bytes when decoding a (valid) string of the
+ * given size.
+ *
+ * @param stringSize size of the string to decode
+ * @return size of the resulting data in bytes
+ */
+ fun calculateDecodedDataLength(stringSize: Int): Int {
+ return stringSize * 5 / 8
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt
new file mode 100644
index 0000000..3a5ecd6
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt
@@ -0,0 +1,89 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import net.taler.wallet.kotlin.exchange.DenominationRecord
+import net.taler.wallet.kotlin.exchange.ExchangeRecord
+
+internal interface Db {
+ suspend fun put(exchange: ExchangeRecord)
+ suspend fun listExchanges(): List<ExchangeRecord>
+ suspend fun getExchangeByBaseUrl(baseUrl: String): ExchangeRecord?
+ suspend fun deleteExchangeByBaseUrl(baseUrl: String)
+ suspend fun put(denomination: DenominationRecord)
+ suspend fun getDenominationsByBaseUrl(baseUrl: String): List<DenominationRecord>
+ suspend fun <T> transaction(function: suspend Db.() -> T): T
+}
+
+internal expect class DbFactory() {
+ fun openDb(): Db
+}
+
+internal class FakeDb : Db {
+
+ private data class Data(
+ val exchanges: HashMap<String, ExchangeRecord> = HashMap(),
+ val denominations: HashMap<String, ArrayList<DenominationRecord>> = HashMap()
+ )
+
+ private var data = Data()
+ private val mutex = Mutex(false)
+
+ override suspend fun put(exchange: ExchangeRecord) {
+ data.exchanges[exchange.baseUrl] = exchange
+ }
+
+ override suspend fun listExchanges(): List<ExchangeRecord> {
+ return data.exchanges.values.toList()
+ }
+
+ override suspend fun getExchangeByBaseUrl(baseUrl: String): ExchangeRecord? {
+ return data.exchanges[baseUrl]
+ }
+
+ override suspend fun deleteExchangeByBaseUrl(baseUrl: String) {
+ data.exchanges.remove(baseUrl)
+ }
+
+ override suspend fun put(denomination: DenominationRecord): Unit = mutex.withLock("transaction") {
+ val list = data.denominations[denomination.exchangeBaseUrl] ?: {
+ val newList = ArrayList<DenominationRecord>()
+ data.denominations[denomination.exchangeBaseUrl] = newList
+ newList
+ }()
+ val index = list.indexOfFirst { it.denomPub == denomination.denomPub }
+ if (index == -1) list.add(denomination)
+ else list[index] = denomination
+ }
+
+ override suspend fun getDenominationsByBaseUrl(baseUrl: String): List<DenominationRecord> {
+ return data.denominations[baseUrl] ?: emptyList()
+ }
+
+ override suspend fun <T> transaction(function: suspend Db.() -> T): T = mutex.withLock("transaction") {
+ val dataCopy = data.copy()
+ return@withLock try {
+ function()
+ } catch (e: Throwable) {
+ data = dataCopy
+ throw e
+ }
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/PaytoUri.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/PaytoUri.kt
new file mode 100644
index 0000000..f6b11d2
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/PaytoUri.kt
@@ -0,0 +1,45 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+data class PaytoUri(
+ val targetType: String,
+ val targetPath: String,
+ val params: Map<String, String>
+) {
+ companion object {
+ private const val SCHEMA = "payto://"
+ fun fromString(s: String): PaytoUri? {
+ if (!s.startsWith(SCHEMA)) return null
+ val rest = s.slice(SCHEMA.length until s.length).split('?')
+ val account = rest[0]
+ val query = if (rest.size > 1) rest[1] else null
+ val firstSlashPos = account.indexOf('/')
+ if (firstSlashPos == -1) return null
+ return PaytoUri(
+ targetType = account.slice(0 until firstSlashPos),
+ targetPath = account.slice((firstSlashPos + 1) until account.length),
+ params = HashMap<String, String>().apply {
+ query?.split('&')?.forEach {
+ val field = it.split('=')
+ if (field.size > 1) put(field[0], field[1])
+ }
+ }
+ )
+ } // end fromString()
+ }
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/TalerUri.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/TalerUri.kt
new file mode 100644
index 0000000..c489d71
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/TalerUri.kt
@@ -0,0 +1,60 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+internal object TalerUri {
+
+ private const val SCHEME = "taler://"
+ private const val SCHEME_INSECURE = "taler+http://"
+ private const val AUTHORITY_PAY = "pay"
+ private const val AUTHORITY_WITHDRAW = "withdraw"
+ private const val AUTHORITY_REFUND = "refund"
+ private const val AUTHORITY_TIP = "tip"
+
+ data class WithdrawUriResult(
+ val bankIntegrationApiBaseUrl: String,
+ val withdrawalOperationId: String
+ )
+
+ /**
+ * Parses a withdraw URI and returns a bank status URL or null if the URI was invalid.
+ */
+ fun parseWithdrawUri(uri: String): WithdrawUriResult? {
+ val (resultScheme, prefix) = when {
+ uri.startsWith(SCHEME, ignoreCase = true) -> {
+ Pair("https://", "${SCHEME}${AUTHORITY_WITHDRAW}/")
+ }
+ uri.startsWith(SCHEME_INSECURE, ignoreCase = true) -> {
+ Pair("http://", "${SCHEME_INSECURE}${AUTHORITY_WITHDRAW}/")
+ }
+ else -> return null
+ }
+ if (!uri.startsWith(prefix)) return null
+ val parts = uri.let {
+ (if (it.endsWith("/")) it.dropLast(1) else it).substring(prefix.length).split('/')
+ }
+ if (parts.size < 2) return null
+ val host = parts[0].toLowerCase()
+ val pathSegments = parts.slice(1 until parts.size - 1).joinToString("/")
+ val withdrawId = parts.last()
+ if (withdrawId.isBlank()) return null
+ val url = "${resultScheme}${host}/${pathSegments}"
+
+ return WithdrawUriResult(url, withdrawId)
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Time.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Time.kt
new file mode 100644
index 0000000..4143389
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Time.kt
@@ -0,0 +1,85 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import com.soywiz.klock.DateTime
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.taler.wallet.kotlin.Duration.Companion.FOREVER
+import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
+import kotlin.math.max
+
+@Serializable
+data class Timestamp(
+ @SerialName("t_ms")
+ val ms: Long
+) : Comparable<Timestamp> {
+
+ companion object {
+ const val NEVER: Long = -1 // TODO or UINT64_MAX?
+ fun now(): Timestamp = Timestamp(DateTime.now().unixMillisLong)
+ }
+
+ /**
+ * Returns a copy of this [Timestamp] rounded to seconds.
+ */
+ fun truncateSeconds(): Timestamp {
+ if (ms == NEVER) return Timestamp(ms)
+ return Timestamp((ms / 1000L) * 1000L)
+ }
+
+ fun roundedToByteArray(): ByteArray = ByteArray(8).apply {
+ (truncateSeconds().ms * 1000L).toByteArray().copyInto(this)
+ }
+
+ operator fun minus(other: Timestamp): Duration = when {
+ ms == NEVER -> Duration(FOREVER)
+ other.ms == NEVER -> throw Error("Invalid argument for timestamp comparision")
+ ms < other.ms -> Duration(0)
+ else -> Duration(ms - other.ms)
+ }
+
+ operator fun minus(other: Duration): Timestamp = when {
+ ms == NEVER -> this
+ other.ms == FOREVER -> Timestamp(0)
+ else -> Timestamp(max(0, ms - other.ms))
+ }
+
+ override fun compareTo(other: Timestamp): Int {
+ return if (ms == NEVER) {
+ if (other.ms == NEVER) 0
+ else 1
+ } else {
+ if (other.ms == NEVER) -1
+ else ms.compareTo(other.ms)
+ }
+ }
+
+}
+
+@Serializable
+data class Duration(
+ /**
+ * Duration in milliseconds.
+ */
+ @SerialName("d_ms")
+ val ms: Long
+) {
+ companion object {
+ const val FOREVER: Long = -1 // TODO or UINT64_MAX?
+ }
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
new file mode 100644
index 0000000..04b17e7
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
@@ -0,0 +1,96 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+
+class CoinRecord(
+ /**
+ * Where did the coin come from? Used for recouping coins.
+ */
+ val coinSource: CoinSourceType,
+
+ /**
+ * Public key of the coin.
+ */
+ val coinPub: String,
+
+ /**
+ * Private key to authorize operations on the coin.
+ */
+ val coinPriv: String,
+
+ /**
+ * Key used by the exchange used to sign the coin.
+ */
+ val denomPub: String,
+
+ /**
+ * Hash of the public key that signs the coin.
+ */
+ val denomPubHash: String,
+
+ /**
+ * Unblinded signature by the exchange.
+ */
+ val denomSig: String,
+
+ /**
+ * Amount that's left on the coin.
+ */
+ val currentAmount: Amount,
+
+ /**
+ * Base URL that identifies the exchange from which we got the coin.
+ */
+ val exchangeBaseUrl: String,
+
+ /**
+ * The coin is currently suspended, and will not be used for payments.
+ */
+ val suspended: Boolean,
+
+ /**
+ * Blinding key used when withdrawing the coin.
+ * Potentially send again during payback.
+ */
+ val blindingKey: String,
+
+ /**
+ * Status of the coin.
+ */
+ val status: CoinStatus
+)
+
+enum class CoinSourceType(val value: String) {
+ WITHDRAW("withdraw"),
+ REFRESH("refresh"),
+ TIP("tip")
+}
+
+enum class CoinStatus(val value: String) {
+
+ /**
+ * Withdrawn and never shown to anybody.
+ */
+ FRESH("fresh"),
+
+ /**
+ * A coin that has been spent and refreshed.
+ */
+ DORMANT("dormant")
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
new file mode 100644
index 0000000..2549195
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
@@ -0,0 +1,41 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import io.ktor.client.HttpClient
+import io.ktor.client.features.json.JsonFeature
+import io.ktor.client.features.json.serializer.KotlinxSerializer
+import io.ktor.client.features.logging.LogLevel
+import io.ktor.client.features.logging.Logging
+import kotlinx.serialization.UnstableDefault
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonConfiguration
+
+fun getDefaultHttpClient(): HttpClient = HttpClient {
+ install(JsonFeature) {
+ serializer = KotlinxSerializer(Json(getJsonConfiguration()))
+ }
+ install(Logging) {
+// level = LogLevel.HEADERS
+ level = LogLevel.NONE
+ }
+}
+
+@OptIn(UnstableDefault::class)
+internal fun getJsonConfiguration() = JsonConfiguration(
+ ignoreUnknownKeys = true
+)
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Version.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Version.kt
new file mode 100644
index 0000000..45e7840
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/Version.kt
@@ -0,0 +1,78 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import kotlin.math.sign
+
+/**
+ * Semantic versioning, but libtool-style.
+ * See https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html
+ */
+
+/**
+ * Result of comparing two libtool versions.
+ */
+data class VersionMatchResult(
+ /**
+ * Is the first version compatible with the second?
+ */
+ val compatible: Boolean,
+ /**
+ * Is the first version older (-1), newer (+1) or identical (0)?
+ */
+ val currentCmp: Int
+)
+
+data class Version(
+ val current: Int,
+ val revision: Int,
+ val age: Int
+)
+
+/**
+ * Compare two libtool-style version strings.
+ */
+fun compareVersions(me: String,other: String): VersionMatchResult? {
+ val meVer = parseVersion (me)
+ val otherVer = parseVersion (other)
+ if (meVer == null || otherVer == null) return null
+
+ val compatible = meVer.current - meVer.age <= otherVer.current &&
+ meVer.current >= otherVer.current - otherVer.age
+
+ val currentCmp = sign((meVer.current - otherVer.current).toDouble()).toInt()
+
+ return VersionMatchResult(compatible, currentCmp)
+}
+
+fun parseVersion(v: String): Version? {
+ val elements = v.split(":")
+ if (elements.size != 3) return null
+ val (currentStr, revisionStr, ageStr) = elements
+ val current = currentStr.toIntOrNull()
+ val revision = revisionStr.toIntOrNull()
+ val age = ageStr.toIntOrNull()
+ if (current == null || revision == null || age == null) return null
+ return Version(current, revision, age)
+}
+
+class SupportedVersions(
+ val walletVersion: Version,
+ val exchangeVersion: Version,
+ val bankVersion: Version,
+ val merchantVersion: Version
+)
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/WalletApi.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/WalletApi.kt
new file mode 100644
index 0000000..11fd181
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/WalletApi.kt
@@ -0,0 +1,101 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import io.ktor.client.HttpClient
+import net.taler.wallet.kotlin.crypto.Crypto
+import net.taler.wallet.kotlin.crypto.CryptoFactory
+import net.taler.wallet.kotlin.crypto.Signature
+import net.taler.wallet.kotlin.exchange.Exchange
+import net.taler.wallet.kotlin.exchange.ExchangeListItem
+import net.taler.wallet.kotlin.exchange.GetExchangeTosResult
+import net.taler.wallet.kotlin.operations.Withdraw
+import net.taler.wallet.kotlin.operations.WithdrawalDetails
+import net.taler.wallet.kotlin.operations.WithdrawalDetailsForUri
+
+public class WalletApi {
+
+ private val httpClient: HttpClient = getDefaultHttpClient()
+ private val db: Db = DbFactory().openDb()
+ private val crypto: Crypto = CryptoFactory.getCrypto()
+ private val signature: Signature = Signature(crypto)
+ private val exchangeManager: Exchange = Exchange(crypto, signature, httpClient, db = db)
+ private val withdrawManager = Withdraw(httpClient, db, crypto, signature, exchangeManager)
+
+ public fun getVersions(): SupportedVersions {
+ return SupportedVersions(
+ walletVersion = Version(8, 0, 0),
+ exchangeVersion = Version(8, 0, 0),
+ bankVersion = Version(0, 0, 0),
+ merchantVersion = Version(1, 0, 0)
+ )
+ }
+
+ public suspend fun getWithdrawalDetailsForUri(talerWithdrawUri: String): WithdrawalDetailsForUri {
+ val bankInfo = withdrawManager.getBankInfo(talerWithdrawUri)
+ return WithdrawalDetailsForUri(
+ amount = bankInfo.amount,
+ defaultExchangeBaseUrl = bankInfo.suggestedExchange,
+ possibleExchanges = emptyList()
+ )
+ }
+
+ public suspend fun getWithdrawalDetailsForAmount(
+ exchangeBaseUrl: String,
+ amount: Amount
+ ): WithdrawalDetails {
+ val details = withdrawManager.getWithdrawalDetails(exchangeBaseUrl, amount)
+ return WithdrawalDetails(
+ tosAccepted = details.exchange.termsOfServiceAccepted,
+ amountRaw = amount,
+ amountEffective = amount - details.overhead - details.withdrawFee
+ )
+ }
+
+ public suspend fun listExchanges(): List<ExchangeListItem> {
+ return db.listExchanges().mapNotNull { exchange ->
+ ExchangeListItem.fromExchangeRecord(exchange)
+ }
+ }
+
+ public suspend fun addExchange(exchangeBaseUrl: String): ExchangeListItem {
+ val exchange = exchangeManager.updateFromUrl(exchangeBaseUrl)
+ db.put(exchange)
+ return ExchangeListItem.fromExchangeRecord(exchange) ?: TODO("error handling")
+ }
+
+ public suspend fun getExchangeTos(exchangeBaseUrl: String): GetExchangeTosResult {
+ val record = db.getExchangeByBaseUrl(exchangeBaseUrl) ?: TODO("error handling")
+ return GetExchangeTosResult(
+ tos = record.termsOfServiceText ?: TODO("error handling"),
+ currentEtag = record.termsOfServiceLastEtag ?: TODO("error handling"),
+ acceptedEtag = record.termsOfServiceAcceptedEtag
+ )
+ }
+
+ public suspend fun setExchangeTosAccepted(exchangeBaseUrl: String, acceptedEtag: String) {
+ db.transaction {
+ val record = getExchangeByBaseUrl(exchangeBaseUrl) ?: TODO("error handling")
+ val updatedRecord = record.copy(
+ termsOfServiceAcceptedEtag = acceptedEtag,
+ termsOfServiceAcceptedTimestamp = Timestamp.now()
+ )
+ put(updatedRecord)
+ }
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Crypto.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Crypto.kt
new file mode 100644
index 0000000..226aa64
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Crypto.kt
@@ -0,0 +1,78 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+internal interface Crypto {
+ fun sha256(input: ByteArray): ByteArray
+ fun sha512(input: ByteArray): ByteArray
+ fun getHashSha512State(): HashSha512State
+ fun getRandomBytes(num: Int): ByteArray
+ fun eddsaGetPublic(eddsaPrivateKey: ByteArray): ByteArray
+ fun ecdheGetPublic(ecdhePrivateKey: ByteArray): ByteArray
+ fun createEddsaKeyPair(): EddsaKeyPair
+ fun createEcdheKeyPair(): EcdheKeyPair
+ fun eddsaSign(msg: ByteArray, eddsaPrivateKey: ByteArray): ByteArray
+ fun eddsaVerify(msg: ByteArray, sig: ByteArray, eddsaPub: ByteArray): Boolean
+ fun keyExchangeEddsaEcdhe(eddsaPrivateKey: ByteArray, ecdhePublicKey: ByteArray): ByteArray
+ fun keyExchangeEcdheEddsa(ecdhePrivateKey: ByteArray, eddsaPublicKey: ByteArray): ByteArray
+ fun kdf(outputLength: Int, ikm: ByteArray, salt: ByteArray, info: ByteArray): ByteArray
+ fun rsaBlind(hm: ByteArray, bks: ByteArray, rsaPubEnc: ByteArray): ByteArray
+ fun rsaUnblind(sig: ByteArray, rsaPubEnc: ByteArray, bks: ByteArray): ByteArray
+ fun rsaVerify(hm: ByteArray, rsaSig: ByteArray, rsaPubEnc: ByteArray): Boolean
+ fun setupRefreshPlanchet(secretSeed: ByteArray, coinNumber: Int): FreshCoin
+}
+
+interface HashSha512State {
+ fun update(data: ByteArray): HashSha512State
+ fun final(): ByteArray
+}
+class EddsaKeyPair(val privateKey: ByteArray, val publicKey: ByteArray)
+class EcdheKeyPair(val privateKey: ByteArray, val publicKey: ByteArray)
+data class FreshCoin(val coinPublicKey: ByteArray, val coinPrivateKey: ByteArray, val bks: ByteArray) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+ other as FreshCoin
+ if (!coinPublicKey.contentEquals(other.coinPublicKey)) return false
+ if (!coinPrivateKey.contentEquals(other.coinPrivateKey)) return false
+ if (!bks.contentEquals(other.bks)) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = coinPublicKey.contentHashCode()
+ result = 31 * result + coinPrivateKey.contentHashCode()
+ result = 31 * result + bks.contentHashCode()
+ return result
+ }
+}
+
+internal expect object CryptoFactory {
+ internal fun getCrypto(): Crypto
+}
+
+private val hexArray = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
+
+fun ByteArray.toHexString(): String {
+ val hexChars = CharArray(this.size * 2)
+ for (j in this.indices) {
+ val v = (this[j].toInt() and 0xFF)
+ hexChars[j * 2] = hexArray[v ushr 4]
+ hexChars[j * 2 + 1] = hexArray[v and 0x0F]
+ }
+ return String(hexChars)
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoImpl.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoImpl.kt
new file mode 100644
index 0000000..0780e45
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoImpl.kt
@@ -0,0 +1,55 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
+
+abstract class CryptoImpl : Crypto {
+
+ companion object {
+ fun Int.toByteArray(): ByteArray {
+ val bytes = ByteArray(4)
+ bytes[3] = (this and 0xFFFF).toByte()
+ bytes[2] = ((this ushr 8) and 0xFFFF).toByte()
+ bytes[1] = ((this ushr 16) and 0xFFFF).toByte()
+ bytes[0] = ((this ushr 24) and 0xFFFF).toByte()
+ return bytes
+ }
+
+ fun Long.toByteArray() = ByteArray(8).apply {
+ var l = this@toByteArray
+ for (i in 7 downTo 0) {
+ this[i] = (l and 0xFF).toByte()
+ l = l shr 8
+ }
+ }
+ }
+
+ override fun kdf(outputLength: Int, ikm: ByteArray, salt: ByteArray, info: ByteArray): ByteArray {
+ return Kdf.kdf(outputLength, ikm, salt, info, { sha256(it) }, { sha512(it) })
+ }
+
+ override fun setupRefreshPlanchet(secretSeed: ByteArray, coinNumber: Int): FreshCoin {
+ val info = "taler-coin-derivation".encodeToByteArray()
+ val salt = coinNumber.toByteArray()
+ val out = kdf(64, secretSeed, salt, info)
+ val coinPrivateKey = out.copyOfRange(0, 32)
+ val bks = out.copyOfRange(32, 64)
+ return FreshCoin(eddsaGetPublic(coinPrivateKey), coinPrivateKey, bks)
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Deposit.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Deposit.kt
new file mode 100644
index 0000000..3156d3f
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Deposit.kt
@@ -0,0 +1,110 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.crypto.Signature.Companion.WALLET_COIN_DEPOSIT
+
+/**
+ * Deposit operations are requested by a merchant during a transaction.
+ * For the deposit operation, the merchant has to obtain the deposit permission for a coin
+ * from their customer who owns the coin.
+ *
+ * When depositing a coin, the merchant is credited an amount specified in the deposit permission,
+ * possibly a fraction of the total coin’s value,
+ * minus the deposit fee as specified by the coin’s denomination.
+ */
+internal class Deposit(private val crypto: Crypto) {
+
+ /**
+ * Private data required to make a deposit permission.
+ */
+ data class DepositInfo(
+ val exchangeBaseUrl: String,
+ val contractTermsHash: String,
+ val coinPublicKey: String,
+ val coinPrivateKey: String,
+ val spendAmount: Amount,
+ val timestamp: Timestamp,
+ val refundDeadline: Timestamp,
+ val merchantPublicKey: String,
+ val feeDeposit: Amount,
+ val wireInfoHash: String,
+ val denomPublicKey: String,
+ val denomSignature: String
+ )
+
+ /**
+ * Deposit permission for a single coin.
+ */
+ // TODO rename _
+ data class CoinDepositPermission(
+ /**
+ * Signature by the coin.
+ */
+ val coinSignature: String,
+ /**
+ * Public key of the coin being spend.
+ */
+ val coinPublicKey: String,
+ /**
+ * Signature made by the denomination public key.
+ */
+ val denomSignature: String,
+ /**
+ * The denomination public key associated with this coin.
+ */
+ val denomPublicKey: String,
+ /**
+ * The amount that is subtracted from this coin with this payment.
+ */
+ val contribution: String,
+ /**
+ * URL of the exchange this coin was withdrawn from.
+ */
+ val exchangeBaseUrl: String
+ )
+
+ /**
+ * Generate updated coins (to store in the database) and deposit permissions for each given coin.
+ */
+ fun signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
+ val d = Signature.PurposeBuilder(WALLET_COIN_DEPOSIT)
+ .put(Base32Crockford.decode(depositInfo.contractTermsHash))
+ .put(Base32Crockford.decode(depositInfo.wireInfoHash))
+ .put(depositInfo.timestamp.roundedToByteArray())
+ .put(depositInfo.refundDeadline.roundedToByteArray())
+ .put(depositInfo.spendAmount.toByteArray())
+ .put(depositInfo.feeDeposit.toByteArray())
+ .put(Base32Crockford.decode(depositInfo.merchantPublicKey))
+ .put(Base32Crockford.decode(depositInfo.coinPublicKey))
+ .build()
+ val coinPriv = Base32Crockford.decode(depositInfo.coinPrivateKey);
+ val coinSig = crypto.eddsaSign(d, coinPriv)
+ return CoinDepositPermission(
+ coinPublicKey = depositInfo.coinPublicKey,
+ coinSignature = Base32Crockford.encode(coinSig),
+ contribution = depositInfo.spendAmount.toJSONString(),
+ denomPublicKey = depositInfo.denomPublicKey,
+ exchangeBaseUrl = depositInfo.exchangeBaseUrl,
+ denomSignature = depositInfo.denomSignature
+ )
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Kdf.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Kdf.kt
new file mode 100644
index 0000000..44f55cc
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Kdf.kt
@@ -0,0 +1,90 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import kotlin.experimental.xor
+import kotlin.math.ceil
+
+internal object Kdf {
+
+ const val HMAC_SHA256_BLOCK_SIZE = 64
+ const val HMAC_SHA512_BLOCK_SIZE = 128
+
+ fun kdf(
+ outputLength: Int,
+ ikm: ByteArray,
+ salt: ByteArray,
+ info: ByteArray,
+ sha256: (ByteArray) -> ByteArray,
+ sha512: (ByteArray) -> ByteArray
+ ): ByteArray {
+ //extract
+ val prk = hmacSha512(salt, ikm, sha512)
+
+ // expand
+ val n = ceil(outputLength.toDouble() / 32).toInt()
+ val output = ByteArray(n * 32)
+ for (i in 0 until n) {
+ val buf: ByteArray
+ if (i == 0) {
+ buf = ByteArray(info.size + 1)
+ info.copyInto(buf)
+ } else {
+ buf = ByteArray(info.size + 1 + 32)
+ for (j in 0 until 32) {
+ buf[j] = output[(i - 1) * 32 + j]
+ }
+ info.copyInto(buf, destinationOffset = 32)
+ }
+ buf[buf.size - 1] = (i + 1).toByte()
+ val chunk = hmacSha256(prk, buf, sha256)
+ chunk.copyInto(output, destinationOffset = i * 32)
+ }
+ return output.copyOfRange(0, outputLength)
+ }
+
+ fun hmacSha256(key: ByteArray, message: ByteArray, sha256: (ByteArray) -> ByteArray): ByteArray {
+ return hmac(HMAC_SHA256_BLOCK_SIZE, key, message) { sha256(it) }
+ }
+
+ fun hmacSha512(key: ByteArray, message: ByteArray, sha512: (ByteArray) -> ByteArray): ByteArray {
+ return hmac(HMAC_SHA512_BLOCK_SIZE, key, message) { sha512(it) }
+ }
+
+ private fun hmac(blockSize: Int, key: ByteArray, message: ByteArray, hash: (ByteArray) -> ByteArray): ByteArray {
+ var newKey = key
+ if (newKey.size > blockSize) newKey = hash(newKey)
+ if (newKey.size < blockSize) newKey = ByteArray(blockSize).apply {
+ newKey.copyInto(this)
+ }
+ val okp = ByteArray(blockSize)
+ val ikp = ByteArray(blockSize)
+ for (i in 0 until blockSize) {
+ ikp[i] = newKey[i] xor 0x36
+ okp[i] = newKey[i] xor 0x5c
+ }
+ val b1 = ByteArray(blockSize + message.size)
+ ikp.copyInto(b1)
+ message.copyInto(b1, destinationOffset = blockSize)
+ val h0 = hash(b1)
+ val b2 = ByteArray(blockSize + h0.size)
+ okp.copyInto(b2)
+ h0.copyInto(b2, destinationOffset = blockSize)
+ return hash(b2)
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt
new file mode 100644
index 0000000..b29007e
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt
@@ -0,0 +1,87 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+
+internal class Planchet(private val crypto: Crypto) {
+
+ data class CreationRequest(
+ val value: Amount,
+ val feeWithdraw: Amount,
+ val denomPub: String,
+ val reservePub: String,
+ val reservePriv: String
+ )
+
+ data class CreationResult(
+ val coinPub: String,
+ val coinPriv: String,
+ val reservePub: String,
+ val denomPubHash: String,
+ val denomPub: String,
+ val blindingKey: String,
+ val withdrawSig: String,
+ val coinEv: String,
+ val coinValue: Amount,
+ val coinEvHash: String
+ )
+
+ internal fun create(req: CreationRequest, coinKeyPair: EddsaKeyPair, blindingFactor: ByteArray): CreationResult {
+ val reservePub = Base32Crockford.decode(req.reservePub)
+ val reservePriv = Base32Crockford.decode(req.reservePriv)
+ val denomPub = Base32Crockford.decode(req.denomPub)
+ val coinPubHash = crypto.sha512(coinKeyPair.publicKey)
+ val ev = crypto.rsaBlind(coinPubHash, blindingFactor, denomPub)
+ val amountWithFee = req.value + req.feeWithdraw
+ val denomPubHash = crypto.sha512(denomPub)
+ val evHash = crypto.sha512(ev)
+
+ val withdrawRequest = Signature.PurposeBuilder(Signature.RESERVE_WITHDRAW)
+ .put(reservePub)
+ .put(amountWithFee.toByteArray())
+ .put(req.feeWithdraw.toByteArray())
+ .put(denomPubHash)
+ .put(evHash)
+ .build()
+
+ val sig = crypto.eddsaSign(withdrawRequest, reservePriv)
+ return CreationResult(
+ blindingKey = Base32Crockford.encode(blindingFactor),
+ coinEv = Base32Crockford.encode(ev),
+ coinPriv = Base32Crockford.encode(coinKeyPair.privateKey),
+ coinPub = Base32Crockford.encode(coinKeyPair.publicKey),
+ coinValue = req.value,
+ denomPub = req.denomPub,
+ denomPubHash = Base32Crockford.encode(denomPubHash),
+ reservePub = req.reservePub,
+ withdrawSig = Base32Crockford.encode(sig),
+ coinEvHash = Base32Crockford.encode(evHash)
+ )
+ }
+
+ /**
+ * Create a pre-coin ([Planchet]) of the given [CreationRequest].
+ */
+ fun create(req: CreationRequest): CreationResult {
+ val coinKeyPair = crypto.createEddsaKeyPair()
+ val blindingFactor = crypto.getRandomBytes(32)
+ return create(req, coinKeyPair, blindingFactor)
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Recoup.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Recoup.kt
new file mode 100644
index 0000000..0f2b6df
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Recoup.kt
@@ -0,0 +1,83 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.CoinRecord
+import net.taler.wallet.kotlin.CoinSourceType.REFRESH
+import net.taler.wallet.kotlin.crypto.Signature.Companion.WALLET_COIN_RECOUP
+
+internal class Recoup(private val crypto: Crypto) {
+
+ /**
+ * Request that we send to the exchange to get a payback.
+ */
+ data class Request(
+ /**
+ * Hashed denomination public key of the coin we want to get
+ * paid back.
+ */
+ val denomPubHash: String,
+
+ /**
+ * Signature over the coin public key by the denomination.
+ */
+ val denomSig: String,
+
+ /**
+ * Coin public key of the coin we want to refund.
+ */
+ val coinPub: String,
+
+ /**
+ * Blinding key that was used during withdraw,
+ * used to prove that we were actually withdrawing the coin.
+ */
+ val coinBlindKeySecret: String,
+
+ /**
+ * Signature made by the coin, authorizing the payback.
+ */
+ val coinSig: String,
+
+ /**
+ * Was the coin refreshed (and thus the recoup should go to the old coin)?
+ */
+ val refreshed: Boolean
+ )
+
+ /**
+ * Create and sign a message to recoup a coin.
+ */
+ fun createRequest(coin: CoinRecord): Request {
+ val p = Signature.PurposeBuilder(WALLET_COIN_RECOUP)
+ .put(Base32Crockford.decode(coin.coinPub))
+ .put(Base32Crockford.decode(coin.denomPubHash))
+ .put(Base32Crockford.decode(coin.blindingKey))
+ .build()
+ val coinSig = crypto.eddsaSign(p, Base32Crockford.decode(coin.coinPriv))
+ return Request(
+ coinBlindKeySecret = coin.blindingKey,
+ coinPub = coin.coinPub,
+ coinSig = Base32Crockford.encode(coinSig),
+ denomPubHash = coin.denomPubHash,
+ denomSig = coin.denomSig,
+ refreshed = coin.coinSource === REFRESH
+ )
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt
new file mode 100644
index 0000000..cd24b07
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt
@@ -0,0 +1,265 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.CoinRecord
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.crypto.Signature.Companion.WALLET_COIN_LINK
+import net.taler.wallet.kotlin.crypto.Signature.Companion.WALLET_COIN_MELT
+import net.taler.wallet.kotlin.crypto.Signature.PurposeBuilder
+import net.taler.wallet.kotlin.exchange.DenominationSelectionInfo
+import net.taler.wallet.kotlin.exchange.SelectedDenomination
+
+internal class Refresh(private val crypto: Crypto) {
+
+ data class RefreshSessionRecord(
+
+ /**
+ * Public key that's being melted in this session.
+ */
+ val meltCoinPub: String,
+
+ /**
+ * How much of the coin's value is melted away with this refresh session?
+ */
+ val amountRefreshInput: Amount,
+
+ /**
+ * Sum of the value of denominations we want to withdraw in this session, without fees.
+ */
+ val amountRefreshOutput: Amount,
+
+ /**
+ * Signature to confirm the melting.
+ */
+ val confirmSig: String,
+
+ /**
+ * Hashed denominations of the newly requested coins.
+ */
+ val newDenominationHashes: List<String>,
+
+ /**
+ * Denominations of the newly requested coins.
+ */
+ val newDenominations: List<String>,
+
+ /**
+ * Planchets for each cut-and-choose instance.
+ */
+ val planchetsForGammas: List<List<RefreshPlanchetRecord>>,
+
+ /**
+ * The transfer public keys, kappa of them.
+ */
+ val transferPublicKeys: List<String>,
+
+ /**
+ * Private keys for the transfer public keys.
+ */
+ val transferPrivateKeys: List<String>,
+
+ /**
+ * The no-reveal-index after we've done the melting.
+ */
+ val noRevealIndex: Int?,
+
+ /**
+ * Hash of the session.
+ */
+ val hash: String,
+
+ /**
+ * Timestamp when the refresh session finished.
+ */
+ val finishedTimestamp: Timestamp?,
+
+ /**
+ * When was this refresh session created?
+ */
+ val timestampCreated: Timestamp,
+
+ /**
+ * Base URL for the exchange we're doing the refresh with.
+ */
+ val exchangeBaseUrl: String
+ )
+
+ data class RefreshPlanchetRecord(
+ /**
+ * Public key for the coin.
+ */
+ val publicKey: String,
+ /**
+ * Private key for the coin.
+ */
+ val privateKey: String,
+ /**
+ * Blinded public key.
+ */
+ val coinEv: String,
+ /**
+ * Blinding key used.
+ */
+ val blindingKey: String
+ )
+
+ /**
+ * Create a new refresh session.
+ */
+ fun createRefreshSession(
+ exchangeBaseUrl: String,
+ meltCoin: CoinRecord,
+ meltFee: Amount,
+ newCoinDenominations: DenominationSelectionInfo,
+ kappa: Int = newCoinDenominations.selectedDenominations.size
+ ) : RefreshSessionRecord {
+ return createRefreshSession(exchangeBaseUrl, meltCoin, meltFee, newCoinDenominations, kappa) {
+ crypto.createEcdheKeyPair()
+ }
+ }
+
+ /**
+ * Create a new refresh session and allow to provide transfer key pairs for testing.
+ */
+ fun createRefreshSession(
+ exchangeBaseUrl: String,
+ meltCoin: CoinRecord,
+ meltFee: Amount,
+ newCoinDenominations: DenominationSelectionInfo,
+ kappa: Int = newCoinDenominations.selectedDenominations.size,
+ kappaKeys: (Int) -> EcdheKeyPair
+ ): RefreshSessionRecord {
+ val sessionHashState = crypto.getHashSha512State()
+
+ // create fresh transfer keys, one pair for each selected denomination (kappa-many)
+ val transferPublicKeys = ArrayList<String>()
+ val transferPrivateKeys = ArrayList<String>()
+ for (i in 0 until kappa) {
+ val transferKeyPair = kappaKeys(i)
+ sessionHashState.update(transferKeyPair.publicKey)
+ transferPrivateKeys.add(Base32Crockford.encode(transferKeyPair.privateKey))
+ transferPublicKeys.add(Base32Crockford.encode(transferKeyPair.publicKey))
+ }
+
+ // add denomination public keys to session hash
+ val newDenominations = ArrayList<String>()
+ val newDenominationHashes = ArrayList<String>()
+ for (selectedDenomination in newCoinDenominations.selectedDenominations) {
+ for (i in 0 until selectedDenomination.count) {
+ newDenominations.add(selectedDenomination.denominationRecord.denomPub)
+ newDenominationHashes.add(selectedDenomination.denominationRecord.denomPubHash)
+ sessionHashState.update(Base32Crockford.decode(selectedDenomination.denominationRecord.denomPub))
+ }
+ }
+
+ // add public key of melted coin to session hash
+ sessionHashState.update(Base32Crockford.decode(meltCoin.coinPub))
+
+ // calculate total value with all fees and add to session hash
+ val (totalOutput, withdrawFee) = calculateOutputAndWithdrawFee(newCoinDenominations.selectedDenominations)
+ val valueWithFee = totalOutput + withdrawFee + meltFee
+ sessionHashState.update(valueWithFee.toByteArray())
+
+ val planchetsForGammas = ArrayList<ArrayList<RefreshPlanchetRecord>>()
+ for (i in 0 until kappa) {
+ val planchets = ArrayList<RefreshPlanchetRecord>()
+ for (selectedDenomination in newCoinDenominations.selectedDenominations) {
+ for (k in 0 until selectedDenomination.count) {
+ val coinNumber = planchets.size
+ val transferPrivateKey = Base32Crockford.decode(transferPrivateKeys[i])
+ val oldCoinPub = Base32Crockford.decode(meltCoin.coinPub)
+ val transferSecret = crypto.keyExchangeEcdheEddsa(transferPrivateKey, oldCoinPub)
+ val fresh = crypto.setupRefreshPlanchet(transferSecret, coinNumber)
+ val publicKeyHash = crypto.sha512(fresh.coinPublicKey)
+ val denominationPub = Base32Crockford.decode(selectedDenomination.denominationRecord.denomPub)
+ val ev = crypto.rsaBlind(publicKeyHash, fresh.bks, denominationPub)
+ val planchet = RefreshPlanchetRecord(
+ blindingKey = Base32Crockford.encode(fresh.bks),
+ coinEv = Base32Crockford.encode(ev),
+ privateKey = Base32Crockford.encode(fresh.coinPrivateKey),
+ publicKey = Base32Crockford.encode(fresh.coinPublicKey)
+ )
+ planchets.add(planchet)
+ sessionHashState.update(ev)
+ }
+ }
+ planchetsForGammas.add(planchets)
+ }
+
+ val sessionHash = sessionHashState.final()
+
+ // make a signature over sessionHash, value (again?), meltFee and meltCoin public key with meltCoin private key
+ val confirmData = PurposeBuilder(WALLET_COIN_MELT)
+ .put(sessionHash)
+ .put(valueWithFee.toByteArray())
+ .put(meltFee.toByteArray())
+ .put(Base32Crockford.decode(meltCoin.coinPub))
+ .build()
+ val confirmSignature = crypto.eddsaSign(confirmData, Base32Crockford.decode(meltCoin.coinPriv))
+
+ return RefreshSessionRecord(
+ confirmSig = Base32Crockford.encode(confirmSignature),
+ exchangeBaseUrl = exchangeBaseUrl,
+ hash = Base32Crockford.encode(sessionHash),
+ meltCoinPub = meltCoin.coinPub,
+ newDenominationHashes = newDenominationHashes,
+ newDenominations = newDenominations,
+ noRevealIndex = null,
+ planchetsForGammas = planchetsForGammas,
+ transferPrivateKeys = transferPrivateKeys,
+ transferPublicKeys = transferPublicKeys,
+ amountRefreshOutput = totalOutput,
+ amountRefreshInput = valueWithFee,
+ timestampCreated = Timestamp.now(),
+ finishedTimestamp = null
+ )
+ }
+
+ private fun calculateOutputAndWithdrawFee(selectedDenomination: List<SelectedDenomination>): Pair<Amount, Amount> {
+ val currency = selectedDenomination[0].denominationRecord.value.currency
+ var total = Amount.zero(currency)
+ var fee = Amount.zero(currency)
+ for (ncd in selectedDenomination) {
+ total += ncd.denominationRecord.value * ncd.count
+ fee += ncd.denominationRecord.feeWithdraw * ncd.count
+ }
+ return Pair(total, fee)
+ }
+
+ fun signCoinLink(
+ oldCoinPrivateKey: String,
+ newDenominationHash: String,
+ oldCoinPublicKey: String,
+ transferPublicKey: String,
+ coinEv: String
+ ): String {
+ val coinEvHash = crypto.sha512(Base32Crockford.decode(coinEv))
+ val coinLink = PurposeBuilder(WALLET_COIN_LINK)
+ .put(Base32Crockford.decode(newDenominationHash))
+ .put(Base32Crockford.decode(oldCoinPublicKey))
+ .put(Base32Crockford.decode(transferPublicKey))
+ .put(coinEvHash)
+ .build()
+ val coinPrivateKey = Base32Crockford.decode(oldCoinPrivateKey)
+ val sig = crypto.eddsaSign(coinLink, coinPrivateKey)
+ return Base32Crockford.encode(sig)
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt
new file mode 100644
index 0000000..9b06756
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt
@@ -0,0 +1,155 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
+import net.taler.wallet.kotlin.exchange.DenominationRecord
+import net.taler.wallet.kotlin.exchange.WireFee
+
+internal class Signature(private val crypto: Crypto) {
+
+ @Suppress("unused")
+ companion object {
+ const val RESERVE_WITHDRAW = 1200
+ const val WALLET_COIN_DEPOSIT = 1201
+ const val MASTER_DENOMINATION_KEY_VALIDITY = 1025
+ const val MASTER_WIRE_FEES = 1028
+ const val MASTER_WIRE_DETAILS = 1030
+ const val WALLET_COIN_MELT = 1202
+ const val TEST = 4242
+ const val MERCHANT_PAYMENT_OK = 1104
+ const val WALLET_COIN_RECOUP = 1203
+ const val WALLET_COIN_LINK = 1204
+ const val EXCHANGE_CONFIRM_RECOUP = 1039
+ const val EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041
+ }
+
+ internal class PurposeBuilder(private val purposeNum: Int) {
+ private val chunks = ArrayList<ByteArray>()
+
+ fun put(bytes: ByteArray): PurposeBuilder {
+ chunks.add(bytes)
+ return this
+ }
+
+ fun build(): ByteArray {
+ var payloadLen = 0
+ for (c in chunks) payloadLen += c.size
+ val size = 4 + 4 + payloadLen
+ val bytes = ByteArray(size)
+ size.toByteArray().copyInto(bytes, 0)
+ purposeNum.toByteArray().copyInto(bytes, 4)
+ var offset = 8
+ for (c in chunks) {
+ c.copyInto(bytes, offset)
+ offset += c.size
+ }
+ return bytes
+ }
+ }
+
+ private fun verifyPayment(sig: ByteArray, contractHash: ByteArray, merchantPub: ByteArray): Boolean {
+ val p = PurposeBuilder(MERCHANT_PAYMENT_OK)
+ .put(contractHash)
+ .build()
+ return crypto.eddsaVerify(p, sig, merchantPub)
+ }
+
+ /**
+ * Verifies an EdDSA payment signature made with [MERCHANT_PAYMENT_OK].
+ *
+ * @param merchantPub an EdDSA public key, usually belonging to a merchant.
+ *
+ * @return true if the signature is valid, false otherwise
+ */
+ fun verifyPayment(sig: String, contractHash: String, merchantPub: String): Boolean {
+ val sigBytes = Base32Crockford.decode(sig)
+ val hashBytes = Base32Crockford.decode(contractHash)
+ val pubBytes = Base32Crockford.decode(merchantPub)
+ return verifyPayment(sigBytes, hashBytes, pubBytes)
+ }
+
+ /**
+ * Verifies an EdDSA wire fee signature made with [MASTER_WIRE_FEES].
+ *
+ * @param masterPub an EdDSA public key
+ *
+ * @return true if the signature is valid, false otherwise
+ */
+ fun verifyWireFee(type: String, wireFee: WireFee, masterPub: String): Boolean {
+ val p = PurposeBuilder(MASTER_WIRE_FEES)
+ .put(crypto.sha512("$type\u0000".encodeToByteArray()))
+ .put(wireFee.startStamp.roundedToByteArray())
+ .put(wireFee.endStamp.roundedToByteArray())
+ .put(wireFee.wireFee.toByteArray())
+ .put(wireFee.closingFee.toByteArray())
+ .build()
+ val sig = Base32Crockford.decode(wireFee.signature)
+ val pub = Base32Crockford.decode(masterPub)
+ return crypto.eddsaVerify(p, sig, pub)
+ }
+
+ /**
+ * Verifies an EdDSA denomination record signature made with [MASTER_DENOMINATION_KEY_VALIDITY].
+ *
+ * @param masterPub an EdDSA public key
+ *
+ * @return true if the signature is valid, false otherwise
+ */
+ fun verifyDenominationRecord(d: DenominationRecord, masterPub: String): Boolean {
+ val pub = Base32Crockford.decode(masterPub)
+ val p = PurposeBuilder(MASTER_DENOMINATION_KEY_VALIDITY)
+ .put(pub)
+ .put(d.stampStart.roundedToByteArray())
+ .put(d.stampExpireWithdraw.roundedToByteArray())
+ .put(d.stampExpireDeposit.roundedToByteArray())
+ .put(d.stampExpireLegal.roundedToByteArray())
+ .put(d.value.toByteArray())
+ .put(d.feeWithdraw.toByteArray())
+ .put(d.feeDeposit.toByteArray())
+ .put(d.feeRefresh.toByteArray())
+ .put(d.feeRefund.toByteArray())
+ .put(Base32Crockford.decode(d.denomPubHash))
+ .build()
+ val sig = Base32Crockford.decode(d.masterSig)
+ return crypto.eddsaVerify(p, sig, pub)
+ }
+
+ /**
+ * Verifies an EdDSA wire account signature made with [MASTER_WIRE_DETAILS].
+ *
+ * @param masterPub an EdDSA public key
+ *
+ * @return true if the signature is valid, false otherwise
+ */
+ fun verifyWireAccount(paytoUri: String, signature: String, masterPub: String): Boolean {
+ val h = crypto.kdf(
+ 64,
+ "exchange-wire-signature".encodeToByteArray(),
+ "$paytoUri\u0000".encodeToByteArray(),
+ ByteArray(0)
+ )
+ val p = PurposeBuilder(MASTER_WIRE_DETAILS)
+ .put(h)
+ .build()
+ val sig = Base32Crockford.decode(signature)
+ val pub = Base32Crockford.decode(masterPub)
+ return crypto.eddsaVerify(p, sig, pub)
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Auditor.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Auditor.kt
new file mode 100644
index 0000000..4df0bdf
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Auditor.kt
@@ -0,0 +1,56 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.exchange
+
+import kotlinx.serialization.Serializable
+
+/**
+ * Auditor information as given by the exchange in /keys.
+ */
+@Serializable
+data class Auditor(
+ /**
+ * Auditor's public key.
+ */
+ val auditor_pub: String,
+
+ /**
+ * Base URL of the auditor.
+ */
+ val auditor_url: String,
+
+ /**
+ * List of signatures for denominations by the auditor.
+ */
+ val denomination_keys: List<AuditorDenomSig>
+)
+
+/**
+ * Signature by the auditor that a particular denomination key is audited.
+ */
+@Serializable
+data class AuditorDenomSig(
+ /**
+ * Denomination public key's hash.
+ */
+ val denom_pub_h: String,
+
+ /**
+ * The signature.
+ */
+ val auditor_sig: String
+)
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Denomination.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Denomination.kt
new file mode 100644
index 0000000..88a81fd
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Denomination.kt
@@ -0,0 +1,234 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.exchange
+
+import kotlinx.serialization.Serializable
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.Duration
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.exchange.DenominationStatus.Unverified
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedGood
+
+/**
+ * Denomination as found in the /keys response from the exchange.
+ */
+@Serializable
+internal data class Denomination(
+ /**
+ * Value of one coin of the denomination.
+ */
+ val value: Amount,
+
+ /**
+ * Public signing key of the denomination.
+ */
+ val denom_pub: String,
+
+ /**
+ * Fee for withdrawing.
+ */
+ val fee_withdraw: Amount,
+
+ /**
+ * Fee for depositing.
+ */
+ val fee_deposit: Amount,
+
+ /**
+ * Fee for refreshing.
+ */
+ val fee_refresh: Amount,
+
+ /**
+ * Fee for refunding.
+ */
+ val fee_refund: Amount,
+
+ /**
+ * Start date from which withdraw is allowed.
+ */
+ val stamp_start: Timestamp,
+
+ /**
+ * End date for withdrawing.
+ */
+ val stamp_expire_withdraw: Timestamp,
+
+ /**
+ * Expiration date after which the exchange can forget about
+ * the currency.
+ */
+ val stamp_expire_legal: Timestamp,
+
+ /**
+ * Date after which the coins of this denomination can't be
+ * deposited anymore.
+ */
+ val stamp_expire_deposit: Timestamp,
+
+ /**
+ * Signature over the denomination information by the exchange's master
+ * signing key.
+ */
+ val master_sig: String
+) {
+ fun toDenominationRecord(
+ baseUrl: String,
+ denomPubHash: ByteArray,
+ isOffered: Boolean,
+ isRevoked: Boolean,
+ status: DenominationStatus
+ ): DenominationRecord =
+ DenominationRecord(
+ denomPub = denom_pub,
+ denomPubHash = Base32Crockford.encode(denomPubHash),
+ exchangeBaseUrl = baseUrl,
+ feeDeposit = fee_deposit,
+ feeRefresh = fee_refresh,
+ feeRefund = fee_refund,
+ feeWithdraw = fee_withdraw,
+ isOffered = isOffered,
+ isRevoked = isRevoked,
+ masterSig = master_sig,
+ stampExpireDeposit = stamp_expire_deposit,
+ stampExpireLegal = stamp_expire_legal,
+ stampExpireWithdraw = stamp_expire_withdraw,
+ stampStart = stamp_start,
+ status = status,
+ value = value
+ )
+}
+
+enum class DenominationStatus {
+ /**
+ * Verification was delayed.
+ */
+ Unverified,
+
+ /**
+ * Verified as valid.
+ */
+ VerifiedGood,
+
+ /**
+ * Verified as invalid.
+ */
+ VerifiedBad
+}
+
+data class DenominationRecord(
+ /**
+ * Value of one coin of the denomination.
+ */
+ val value: Amount,
+ /**
+ * The denomination public key.
+ */
+ val denomPub: String,
+ /**
+ * Hash of the denomination public key.
+ * Stored in the database for faster lookups.
+ */
+ val denomPubHash: String,
+ /**
+ * Fee for withdrawing.
+ */
+ val feeWithdraw: Amount,
+ /**
+ * Fee for depositing.
+ */
+ val feeDeposit: Amount,
+ /**
+ * Fee for refreshing.
+ */
+ val feeRefresh: Amount,
+ /**
+ * Fee for refunding.
+ */
+ val feeRefund: Amount,
+ /**
+ * Validity start date of the denomination.
+ */
+ val stampStart: Timestamp,
+ /**
+ * Date after which the currency can't be withdrawn anymore.
+ */
+ val stampExpireWithdraw: Timestamp,
+ /**
+ * Date after the denomination officially doesn't exist anymore.
+ */
+ val stampExpireLegal: Timestamp,
+ /**
+ * Data after which coins of this denomination can't be deposited anymore.
+ */
+ val stampExpireDeposit: Timestamp,
+ /**
+ * Signature by the exchange's master key over the denomination
+ * information.
+ */
+ val masterSig: String,
+ /**
+ * Did we verify the signature on the denomination?
+ */
+ val status: DenominationStatus,
+ /**
+ * Was this denomination still offered by the exchange the last time
+ * we checked?
+ * Only false when the exchange redacts a previously published denomination.
+ */
+ val isOffered: Boolean,
+ /**
+ * Did the exchange revoke the denomination?
+ * When this field is set to true in the database, the same transaction
+ * should also mark all affected coins as revoked.
+ */
+ val isRevoked: Boolean,
+ /**
+ * Base URL of the exchange.
+ */
+ val exchangeBaseUrl: String
+) {
+ fun isWithdrawable(now: Timestamp = Timestamp.now()): Boolean {
+ if (isRevoked) return false // can not use revoked denomination
+ if (status != Unverified && status != VerifiedGood) return false // verified to be bad
+ if (now < stampStart) return false // denomination has not yet started
+ val lastPossibleWithdraw = stampExpireWithdraw - Duration(50 * 1000)
+ if ((lastPossibleWithdraw - now).ms == 0L) return false // denomination has expired
+ return true
+ }
+}
+
+data class DenominationSelectionInfo(
+ val totalCoinValue: Amount,
+ val totalWithdrawCost: Amount,
+ val selectedDenominations: List<SelectedDenomination>
+) {
+ fun getEarliestDepositExpiry(): Timestamp {
+ if (selectedDenominations.isEmpty()) return Timestamp(
+ Timestamp.NEVER
+ )
+ var earliest = selectedDenominations[0].denominationRecord.stampExpireDeposit
+ for (i in 1 until selectedDenominations.size) {
+ val stampExpireDeposit = selectedDenominations[i].denominationRecord.stampExpireDeposit
+ if (stampExpireDeposit < earliest) earliest = stampExpireDeposit
+ }
+ return earliest
+ }
+}
+
+data class SelectedDenomination(val count: Int, val denominationRecord: DenominationRecord)
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Exchange.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Exchange.kt
new file mode 100644
index 0000000..7a6ac7f
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Exchange.kt
@@ -0,0 +1,262 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.exchange
+
+import io.ktor.client.HttpClient
+import io.ktor.client.request.accept
+import io.ktor.client.request.get
+import io.ktor.client.statement.HttpResponse
+import io.ktor.client.statement.readText
+import io.ktor.http.ContentType
+import io.ktor.http.HttpHeaders
+import io.ktor.http.HttpStatusCode
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.Db
+import net.taler.wallet.kotlin.DbFactory
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.compareVersions
+import net.taler.wallet.kotlin.crypto.Crypto
+import net.taler.wallet.kotlin.crypto.CryptoFactory
+import net.taler.wallet.kotlin.crypto.Signature
+import net.taler.wallet.kotlin.exchange.DenominationStatus.Unverified
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateReason.Initial
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateStatus.FetchKeys
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateStatus.FetchTerms
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateStatus.FetchWire
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateStatus.FinalizeUpdate
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateStatus.Finished
+import net.taler.wallet.kotlin.getDefaultHttpClient
+
+internal class Exchange(
+ private val crypto: Crypto = CryptoFactory.getCrypto(),
+ private val signature: Signature = Signature(crypto),
+ private val httpClient: HttpClient = getDefaultHttpClient(),
+ // using the default Http client adds a json Accept header to each request, so we need a different one
+ // because the exchange is returning XML when it doesn't exactly match a mime type.
+ private val httpNoJsonClient: HttpClient = HttpClient(),
+ private val db: Db = DbFactory().openDb()
+) {
+
+ companion object {
+ private const val PROTOCOL_VERSION = "8:0:0"
+ fun getVersionMatch(version: String) = compareVersions(PROTOCOL_VERSION, version)
+ fun normalizeUrl(exchangeBaseUrl: String): String {
+ var url = exchangeBaseUrl
+ if (!url.startsWith("http")) url = "http://$url"
+ if (!url.endsWith("/")) url = "$url/"
+ // TODO also remove query and hash
+ return url
+ }
+ }
+
+ /**
+ * Update or add exchange DB entry by fetching the /keys, /wire and /terms information.
+ */
+ suspend fun updateFromUrl(baseUrl: String): ExchangeRecord {
+ val now = Timestamp.now()
+ val url = normalizeUrl(baseUrl)
+ var record = db.getExchangeByBaseUrl(url) ?: ExchangeRecord(
+ baseUrl = url,
+ timestampAdded = now,
+ updateStatus = FetchKeys,
+ updateStarted = now,
+ updateReason = Initial
+ ).also { db.put(it) }
+ val recordBeforeUpdate = record.copy()
+
+ record = updateKeys(record) // TODO add denominations in transaction at the end
+ record = updateWireInfo(record)
+ record = updateTermsOfService(record)
+ record = finalizeUpdate(record)
+ db.transaction {
+ val dbRecord = getExchangeByBaseUrl(record.baseUrl)
+ if (dbRecord != recordBeforeUpdate) throw Error("Concurrent modification of $dbRecord")
+ put(record)
+ }
+ return record
+ }
+
+ /**
+ * Fetch the exchange's /keys and update database accordingly.
+ *
+ * Exceptions thrown in this method must be caught and reported in the pending operations.
+ */
+ internal suspend fun updateKeys(record: ExchangeRecord): ExchangeRecord {
+ val keys: Keys = Keys.fetch(httpClient, record.baseUrl)
+ // check if there are denominations offered
+ // TODO provide more error information for catcher
+ if (keys.denoms.isEmpty()) {
+ throw Error("Exchange doesn't offer any denominations")
+ }
+ // check if the exchange version is compatible
+ val versionMatch = getVersionMatch(keys.version)
+ if (versionMatch == null || !versionMatch.compatible) {
+ throw Error("Exchange protocol version not compatible with wallet")
+ }
+ val currency = keys.denoms[0].value.currency
+ val newDenominations = keys.denoms.map { d ->
+ getDenominationRecord(record.baseUrl, currency, d)
+ }
+ // update exchange details
+ val details = ExchangeDetails(
+ auditors = keys.auditors,
+ currency = currency,
+ lastUpdateTime = keys.list_issue_date,
+ masterPublicKey = keys.master_public_key,
+ protocolVersion = keys.version,
+ signingKeys = keys.signkeys
+ )
+ val updatedRecord = record.copy(details = details, updateStatus = FetchWire)
+ for (newDenomination in newDenominations) {
+ // TODO check oldDenominations and do consistency checks
+ db.put(newDenomination)
+ }
+
+ // TODO handle keys.recoup
+
+ return updatedRecord
+ }
+
+ /**
+ * Turn an exchange's denominations from /keys into [DenominationRecord]s
+ *
+ * Visible for testing.
+ */
+ internal fun getDenominationRecord(baseUrl: String, currency: String, d: Denomination): DenominationRecord {
+ checkCurrency(currency, d.value)
+ checkCurrency(currency, d.fee_refund)
+ checkCurrency(currency, d.fee_withdraw)
+ checkCurrency(currency, d.fee_refresh)
+ checkCurrency(currency, d.fee_deposit)
+ return d.toDenominationRecord(
+ baseUrl = baseUrl,
+ denomPubHash = crypto.sha512(Base32Crockford.decode(d.denom_pub)),
+ isOffered = true,
+ isRevoked = false,
+ status = Unverified
+ )
+ }
+
+ /**
+ * Fetch wire information for an exchange and store it in the database.
+ */
+ internal suspend fun updateWireInfo(record: ExchangeRecord): ExchangeRecord {
+ if (record.updateStatus != FetchWire) {
+ throw Error("Unexpected updateStatus: ${record.updateStatus}, expected: $FetchWire")
+ }
+ if (record.details == null) throw Error("Invalid exchange state")
+ val wire = Wire.fetch(httpClient, record.baseUrl)
+ // check account signatures
+ for (a in wire.accounts) {
+ val valid = signature.verifyWireAccount(
+ paytoUri = a.paytoUri,
+ signature = a.masterSig,
+ masterPub = record.details.masterPublicKey
+ )
+ if (!valid) throw Error("Exchange wire account signature invalid")
+ }
+ // check fee signatures
+ for (fee in wire.fees) {
+ val wireMethod = fee.key
+ val wireFees = fee.value
+ for (wireFee in wireFees) {
+ val valid = signature.verifyWireFee(
+ type = wireMethod,
+ wireFee = wireFee,
+ masterPub = record.details.masterPublicKey
+ )
+ if (!valid) throw Error("Exchange wire fee signature invalid")
+ checkCurrency(record.details.currency, wireFee.wireFee)
+ checkCurrency(record.details.currency, wireFee.closingFee)
+ }
+ }
+ val wireInfo = ExchangeWireInfo(
+ accounts = wire.accounts.map { ExchangeBankAccount(it.paytoUri) },
+ feesForType = wire.fees
+ )
+ return record.copy(updateStatus = FetchTerms, wireInfo = wireInfo)
+ }
+
+ /**
+ * Fetch wire information for an exchange and store it in the database.
+ */
+ internal suspend fun updateTermsOfService(record: ExchangeRecord): ExchangeRecord {
+ if (record.updateStatus != FetchTerms) {
+ throw Error("Unexpected updateStatus: ${record.updateStatus}, expected: $FetchTerms")
+ }
+ val response: HttpResponse = httpNoJsonClient.get("${record.baseUrl}terms") {
+ accept(ContentType.Text.Plain)
+ }
+ if (response.status != HttpStatusCode.OK) {
+ throw Error("/terms response has unexpected status code (${response.status.value})")
+ }
+ val text = response.readText()
+ val eTag = response.headers[HttpHeaders.ETag]
+ return record.copy(updateStatus = FinalizeUpdate, termsOfServiceText = text, termsOfServiceLastEtag = eTag)
+ }
+
+ internal fun finalizeUpdate(record: ExchangeRecord): ExchangeRecord {
+ if (record.updateStatus != FinalizeUpdate) {
+ throw Error("Unexpected updateStatus: ${record.updateStatus}, expected: $FinalizeUpdate")
+ }
+ // TODO store an event log for this update (exchangeUpdatedEvents)
+ return record.copy(updateStatus = Finished, addComplete = true)
+ }
+
+ private fun checkCurrency(currency: String, amount: Amount) {
+ if (currency != amount.currency) throw Error("Expected currency $currency, but found ${amount.currency}")
+ }
+
+}
+
+
+data class ExchangeListItem(
+ val exchangeBaseUrl: String,
+ val currency: String,
+ val paytoUris: List<String>
+) {
+ companion object {
+ fun fromExchangeRecord(exchange: ExchangeRecord): ExchangeListItem? {
+ return if (exchange.details == null || exchange.wireInfo == null) null
+ else ExchangeListItem(
+ exchangeBaseUrl = exchange.baseUrl,
+ currency = exchange.details.currency,
+ paytoUris = exchange.wireInfo.accounts.map {
+ it.paytoUri
+ }
+ )
+ }
+ }
+}
+
+data class GetExchangeTosResult(
+ /**
+ * Markdown version of the current ToS.
+ */
+ val tos: String,
+
+ /**
+ * Version tag of the current ToS.
+ */
+ val currentEtag: String,
+
+ /**
+ * Version tag of the last ToS that the user has accepted, if any.
+ */
+ val acceptedEtag: String? = null
+)
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/ExchangeRecord.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/ExchangeRecord.kt
new file mode 100644
index 0000000..9bfd649
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/ExchangeRecord.kt
@@ -0,0 +1,151 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.exchange
+
+import net.taler.wallet.kotlin.Timestamp
+
+/**
+ * Exchange record as stored in the wallet's database.
+ */
+data class ExchangeRecord(
+ /**
+ * Base url of the exchange.
+ */
+ val baseUrl: String,
+
+ /**
+ * Did we finish adding the exchange?
+ */
+ val addComplete: Boolean = false,
+
+ /**
+ * Was the exchange added as a built-in exchange?
+ */
+ val builtIn: Boolean = false,
+
+ /**
+ * Details, once known.
+ */
+ val details: ExchangeDetails? = null,
+
+ /**
+ * Mapping from wire method type to the wire fee.
+ */
+ val wireInfo: ExchangeWireInfo? = null,
+
+ /**
+ * When was the exchange added to the wallet?
+ */
+ val timestampAdded: Timestamp,
+
+ /**
+ * Terms of service text or undefined if not downloaded yet.
+ */
+ val termsOfServiceText: String? = null,
+
+ /**
+ * ETag for last terms of service download.
+ */
+ val termsOfServiceLastEtag: String? = null,
+
+ /**
+ * ETag for last terms of service download.
+ */
+ val termsOfServiceAcceptedEtag: String? = null,
+
+ /**
+ * ETag for last terms of service download.
+ */
+ val termsOfServiceAcceptedTimestamp: Timestamp? = null,
+
+ /**
+ * Time when the update to the exchange has been started or
+ * undefined if no update is in progress.
+ */
+ val updateStarted: Timestamp? = null,
+
+ val updateStatus: ExchangeUpdateStatus,
+
+ val updateReason: ExchangeUpdateReason? = null
+) {
+ init {
+ check(baseUrl == Exchange.normalizeUrl(baseUrl)) { "Base URL was not normalized" }
+ }
+
+ val termsOfServiceAccepted: Boolean
+ get() = termsOfServiceAcceptedTimestamp != null && termsOfServiceAcceptedEtag == termsOfServiceLastEtag
+}
+
+/**
+ * Details about the exchange that we only know after querying /keys and /wire.
+ */
+data class ExchangeDetails(
+ /**
+ * Master public key of the exchange.
+ */
+ val masterPublicKey: String,
+
+ /**
+ * Auditors (partially) auditing the exchange.
+ */
+ val auditors: List<Auditor>,
+
+ /**
+ * Currency that the exchange offers.
+ */
+ val currency: String,
+
+ /**
+ * Last observed protocol version.
+ */
+ val protocolVersion: String,
+
+ /**
+ * Signing keys we got from the exchange, can also contain
+ * older signing keys that are not returned by /keys anymore.
+ */
+ val signingKeys: List<SigningKey>,
+
+ /**
+ * Timestamp for last update.
+ */
+ val lastUpdateTime: Timestamp
+)
+
+data class ExchangeWireInfo(
+ val feesForType: Map<String, List<WireFee>>,
+ val accounts: List<ExchangeBankAccount>
+)
+
+// TODO is this class needed?
+data class ExchangeBankAccount(
+ val paytoUri: String
+)
+
+sealed class ExchangeUpdateStatus(val value: String) {
+ object FetchKeys : ExchangeUpdateStatus("fetch-keys")
+ object FetchWire : ExchangeUpdateStatus("fetch-wire")
+ object FetchTerms : ExchangeUpdateStatus("fetch-terms")
+ object FinalizeUpdate : ExchangeUpdateStatus("finalize-update")
+ object Finished : ExchangeUpdateStatus("finished")
+}
+
+sealed class ExchangeUpdateReason(val value: String) {
+ object Initial : ExchangeUpdateReason("initial")
+ object Forced : ExchangeUpdateReason("forced")
+ object Scheduled : ExchangeUpdateReason("scheduled")
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Keys.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Keys.kt
new file mode 100644
index 0000000..54806f9
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Keys.kt
@@ -0,0 +1,97 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.exchange
+
+import io.ktor.client.HttpClient
+import io.ktor.client.request.get
+import kotlinx.serialization.Serializable
+import net.taler.wallet.kotlin.Timestamp
+
+/**
+ * Structure that the exchange gives us in /keys.
+ */
+@Serializable
+internal data class Keys(
+ /**
+ * List of offered denominations.
+ */
+ val denoms: List<Denomination>,
+
+ /**
+ * The exchange's master public key.
+ */
+ val master_public_key: String,
+
+ /**
+ * The list of auditors (partially) auditing the exchange.
+ */
+ val auditors: List<Auditor>,
+
+ /**
+ * Timestamp when this response was issued.
+ */
+ val list_issue_date: Timestamp,
+
+ /**
+ * List of revoked denominations.
+ */
+ val recoup: List<Recoup>?,
+
+ /**
+ * Short-lived signing keys used to sign online
+ * responses.
+ */
+ val signkeys: List<SigningKey>,
+
+ /**
+ * Protocol version.
+ */
+ val version: String
+) {
+ companion object {
+ /**
+ * Fetch an exchange's /keys with the given normalized base URL.
+ */
+ suspend fun fetch(httpClient: HttpClient, exchangeBaseUrl: String): Keys {
+ return httpClient.get("${exchangeBaseUrl}keys")
+ }
+ }
+}
+
+/**
+ * Structure of one exchange signing key in the /keys response.
+ */
+@Serializable
+data class SigningKey(
+ val stamp_start: Timestamp,
+ val stamp_expire: Timestamp,
+ val stamp_end: Timestamp,
+ val key: String,
+ val master_sig: String
+)
+
+/**
+ * Element of the payback list that the
+ * exchange gives us in /keys.
+ */
+@Serializable
+data class Recoup(
+ /**
+ * The hash of the denomination public key for which the payback is offered.
+ */
+ val h_denom_pub: String
+)
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Wire.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Wire.kt
new file mode 100644
index 0000000..c8fae88
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Wire.kt
@@ -0,0 +1,79 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.exchange
+
+import io.ktor.client.HttpClient
+import io.ktor.client.request.get
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Timestamp
+
+@Serializable
+internal data class Wire(
+ val accounts: List<AccountInfo>,
+ val fees: Map<String, List<WireFee>>
+) {
+ companion object {
+ /**
+ * Fetch an exchange's /wire with the given normalized base URL.
+ */
+ suspend fun fetch(httpClient: HttpClient, exchangeBaseUrl: String): Wire {
+ return httpClient.get("${exchangeBaseUrl}wire")
+ }
+ }
+}
+
+@Serializable
+data class AccountInfo(
+ @SerialName("payto_uri")
+ val paytoUri: String,
+ @SerialName("master_sig")
+ val masterSig: String
+)
+
+/**
+ * Wire fees as announced by the exchange.
+ */
+@Serializable
+data class WireFee(
+ /**
+ * Fee for wire transfers.
+ */
+ @SerialName("wire_fee")
+ val wireFee: Amount,
+ /**
+ * Fees to close and refund a reserve.
+ */
+ @SerialName("closing_fee")
+ val closingFee: Amount,
+ /**
+ * Start date of the fee.
+ */
+ @SerialName("start_date")
+ val startStamp: Timestamp,
+ /**
+ * End date of the fee.
+ */
+ @SerialName("end_date")
+ val endStamp: Timestamp,
+ /**
+ * Signature made by the exchange master key.
+ */
+ @SerialName("sig")
+ val signature: String
+)
diff --git a/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/operations/Withdraw.kt b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/operations/Withdraw.kt
new file mode 100644
index 0000000..e51e9ec
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/wallet/kotlin/operations/Withdraw.kt
@@ -0,0 +1,305 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.operations
+
+import io.ktor.client.HttpClient
+import io.ktor.client.request.get
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Db
+import net.taler.wallet.kotlin.DbFactory
+import net.taler.wallet.kotlin.TalerUri.parseWithdrawUri
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.VersionMatchResult
+import net.taler.wallet.kotlin.crypto.Crypto
+import net.taler.wallet.kotlin.crypto.CryptoFactory
+import net.taler.wallet.kotlin.crypto.Signature
+import net.taler.wallet.kotlin.exchange.DenominationRecord
+import net.taler.wallet.kotlin.exchange.DenominationSelectionInfo
+import net.taler.wallet.kotlin.exchange.DenominationStatus.Unverified
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedBad
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedGood
+import net.taler.wallet.kotlin.exchange.Exchange
+import net.taler.wallet.kotlin.exchange.ExchangeListItem
+import net.taler.wallet.kotlin.exchange.ExchangeRecord
+import net.taler.wallet.kotlin.exchange.ExchangeWireInfo
+import net.taler.wallet.kotlin.exchange.SelectedDenomination
+import net.taler.wallet.kotlin.getDefaultHttpClient
+
+internal class Withdraw(
+ private val httpClient: HttpClient = getDefaultHttpClient(),
+ private val db: Db = DbFactory().openDb(),
+ private val crypto: Crypto = CryptoFactory.getCrypto(),
+ private val signature: Signature = Signature(crypto),
+ private val exchange: Exchange = Exchange(crypto, signature, httpClient, db = db)
+) {
+
+ data class BankDetails(
+ val amount: Amount,
+ val selectionDone: Boolean,
+ val transferDone: Boolean,
+ val senderPaytoUri: String?,
+ val suggestedExchange: String?,
+ val confirmTransferUrl: String?,
+ val wireTypes: List<String>,
+ val extractedStatusUrl: String
+ )
+
+ @Serializable
+ data class Response(
+ @SerialName("selection_done")
+ val selectionDone: Boolean,
+ @SerialName("transfer_done")
+ val transferDone: Boolean,
+ val amount: Amount,
+ @SerialName("wire_types")
+ val wireTypes: List<String>,
+ @SerialName("sender_wire")
+ val senderPaytoUri: String?,
+ @SerialName("suggested_exchange")
+ val suggestedExchange: String?,
+ @SerialName("confirm_transfer_url")
+ val confirmTransferUrl: String?
+ ) {
+ fun toBankDetails(extractedStatusUrl: String) = BankDetails(
+ amount = amount,
+ confirmTransferUrl = confirmTransferUrl,
+ extractedStatusUrl = extractedStatusUrl,
+ selectionDone = selectionDone,
+ senderPaytoUri = senderPaytoUri,
+ suggestedExchange = suggestedExchange,
+ transferDone = transferDone,
+ wireTypes = wireTypes
+ )
+ }
+
+ suspend fun getBankInfo(talerWithdrawUri: String): BankDetails {
+ val uriResult =
+ parseWithdrawUri(talerWithdrawUri) ?: throw Error("Can't parse URI $talerWithdrawUri")
+ val url =
+ "${uriResult.bankIntegrationApiBaseUrl}api/withdraw-operation/${uriResult.withdrawalOperationId}"
+ val response: Response = httpClient.get(url)
+ return response.toBankDetails(url)
+ }
+
+ /**
+ * Information about what will happen when creating a reserve.
+ *
+ * Sent to the wallet frontend to be rendered and shown to the user.
+ */
+ data class WithdrawalDetails(
+ /**
+ * Exchange that the reserve will be created at.
+ */
+ // TODO we probably don't need to include our internal exchange record in here
+ val exchange: ExchangeRecord,
+
+ /**
+ * Selected denominations for withdraw.
+ */
+ val selectedDenominations: DenominationSelectionInfo,
+
+ /**
+ * Fees for withdraw.
+ */
+ val withdrawFee: Amount,
+
+ /**
+ * Remaining balance that is too small to be withdrawn.
+ */
+ val overhead: Amount,
+
+ /**
+ * The earliest deposit expiration of the selected coins.
+ */
+ // TODO what is this needed for?
+ val earliestDepositExpiration: Timestamp,
+
+ /**
+ * Number of currently offered denominations.
+ */
+ // TODO what is this needed for?
+ val numOfferedDenoms: Int
+ ) {
+ init {
+ check(exchange.details != null)
+ check(exchange.wireInfo != null)
+ }
+
+ /**
+ * Filtered wire info to send to the bank.
+ */
+ val exchangeWireAccounts: List<String> get() = exchange.wireInfo!!.accounts.map { it.paytoUri }
+
+ /**
+ * Wire fees from the exchange.
+ */
+ val wireFees: ExchangeWireInfo get() = exchange.wireInfo!!
+
+ /**
+ * Did the user already accept the current terms of service for the exchange?
+ */
+ val termsOfServiceAccepted: Boolean get() = exchange.termsOfServiceAccepted
+
+ /**
+ * Result of checking the wallet's version against the exchange's version.
+ */
+ val versionMatch: VersionMatchResult?
+ get() = Exchange.getVersionMatch(exchange.details!!.protocolVersion)
+
+ }
+
+ internal suspend fun getWithdrawalDetails(
+ exchangeBaseUrl: String,
+ amount: Amount
+ ): WithdrawalDetails {
+ val exchange = exchange.updateFromUrl(exchangeBaseUrl)
+ check(exchange.details != null)
+ check(exchange.wireInfo != null)
+ val selectedDenominations = selectDenominations(exchange, amount)
+ val possibleDenominations =
+ db.getDenominationsByBaseUrl(exchangeBaseUrl).filter { it.isOffered }
+ // TODO determine trust and audit status
+ return WithdrawalDetails(
+ exchange = exchange,
+ selectedDenominations = selectedDenominations,
+ withdrawFee = selectedDenominations.totalWithdrawCost - selectedDenominations.totalCoinValue,
+ overhead = amount - selectedDenominations.totalWithdrawCost,
+ earliestDepositExpiration = selectedDenominations.getEarliestDepositExpiry(),
+ numOfferedDenoms = possibleDenominations.size
+ )
+ }
+
+ /**
+ * Get a list of denominations to withdraw from the given exchange for the given amount,
+ * making sure that all denominations' signatures are verified.
+ */
+ internal suspend fun selectDenominations(
+ exchange: ExchangeRecord,
+ amount: Amount
+ ): DenominationSelectionInfo {
+ val exchangeDetails =
+ exchange.details ?: throw Error("Exchange $exchange details not available.")
+
+ val possibleDenominations = getPossibleDenominations(exchange.baseUrl)
+ val selectedDenominations = getDenominationSelection(amount, possibleDenominations)
+ // TODO consider validating denominations before writing them into the DB
+ for (selectedDenomination in selectedDenominations.selectedDenominations) {
+ var denomination = selectedDenomination.denominationRecord
+ if (denomination.status == Unverified) {
+ val valid = signature.verifyDenominationRecord(
+ denomination,
+ exchangeDetails.masterPublicKey
+ )
+ denomination = if (!valid) {
+ denomination.copy(status = VerifiedBad)
+ } else {
+ denomination.copy(status = VerifiedGood)
+ }
+ db.put(denomination)
+ }
+ if (denomination.status == VerifiedBad) throw Error("Exchange $exchange has bad denomination.")
+ }
+ return selectedDenominations
+ }
+
+ suspend fun getPossibleDenominations(exchangeBaseUrl: String): List<DenominationRecord> {
+ return db.getDenominationsByBaseUrl(exchangeBaseUrl).filter { denomination ->
+ (denomination.status == Unverified || denomination.status == VerifiedGood) &&
+ !denomination.isRevoked
+ }
+ }
+
+ /**
+ * Get a list of denominations (with repetitions possible)
+ * whose total value is as close as possible to the available amount, but never larger.
+ *
+ * Note that this algorithm does not try to optimize withdrawal fees.
+ */
+ fun getDenominationSelection(
+ amount: Amount,
+ denoms: List<DenominationRecord>
+ ): DenominationSelectionInfo {
+ val selectedDenominations = ArrayList<SelectedDenomination>()
+ var totalCoinValue = Amount.zero(amount.currency)
+ var totalWithdrawCost = Amount.zero(amount.currency)
+
+ // denominations need to be sorted, so we try the highest ones first
+ val now = Timestamp.now()
+ val denominations = denoms.filter { it.isWithdrawable(now) }.sortedByDescending { it.value }
+ var remainingAmount = amount.copy()
+ val zero = Amount.zero(amount.currency)
+ for (d in denominations) {
+ var count = 0
+ val totalCost = d.value + d.feeWithdraw
+ // keep adding this denomination as long as its total cost fits into remaining amount
+ while (remainingAmount >= totalCost) {
+ remainingAmount -= totalCost
+ count++
+ }
+ // calculate new totals based on count-many added denominations
+ if (count > 0) {
+ totalCoinValue += d.value * count
+ totalWithdrawCost += totalCost * count
+ selectedDenominations.add(SelectedDenomination(count, d))
+ }
+ // stop early if nothing is remaining
+ if (remainingAmount == zero) break
+ }
+ return DenominationSelectionInfo(
+ selectedDenominations = selectedDenominations,
+ totalCoinValue = totalCoinValue,
+ totalWithdrawCost = totalWithdrawCost
+ )
+ }
+
+}
+
+data class WithdrawalDetailsForUri(
+ /**
+ * The amount that the user wants to withdraw
+ */
+ val amount: Amount,
+
+ /**
+ * Exchange suggested by the wallet
+ */
+ val defaultExchangeBaseUrl: String?,
+
+ /**
+ * A list of exchanges that can be used for this withdrawal
+ */
+ val possibleExchanges: List<ExchangeListItem>
+)
+
+data class WithdrawalDetails(
+ /**
+ * Did the user accept the current version of the exchange's terms of service?
+ */
+ val tosAccepted: Boolean,
+
+ /**
+ * Amount that will be transferred to the exchange.
+ */
+ val amountRaw: Amount,
+
+ /**
+ * Amount that will be added to the user's wallet balance.
+ */
+ val amountEffective: Amount
+)
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/AmountTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/AmountTest.kt
new file mode 100644
index 0000000..08ee618
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/AmountTest.kt
@@ -0,0 +1,276 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+class AmountTest {
+
+ companion object {
+ private val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
+ fun getRandomString(minLength: Int = 1, maxLength: Int = Random.nextInt(0, 1337)) = (minLength..maxLength)
+ .map { Random.nextInt(0, charPool.size) }
+ .map(charPool::get)
+ .joinToString("")
+
+ fun getRandomAmount() = getRandomAmount(getRandomString(1, Random.nextInt(1, 12)))
+
+ fun getRandomAmount(currency: String): Amount {
+ val value = Random.nextLong(0, Amount.MAX_VALUE)
+ val fraction = Random.nextInt(0, Amount.MAX_FRACTION)
+ return Amount(currency, value, fraction)
+ }
+ }
+
+ @Test
+ fun testFromJSONString() {
+ var str = "TESTKUDOS:23.42"
+ var amount = Amount.fromJSONString(str)
+ assertEquals(str, amount.toJSONString())
+ assertEquals("TESTKUDOS", amount.currency)
+ assertEquals(23, amount.value)
+ assertEquals((0.42 * 1e8).toInt(), amount.fraction)
+ assertEquals("23.42 TESTKUDOS", amount.toString())
+
+ str = "EUR:500000000.00000001"
+ amount = Amount.fromJSONString(str)
+ assertEquals(str, amount.toJSONString())
+ assertEquals("EUR", amount.currency)
+ assertEquals(500000000, amount.value)
+ assertEquals(1, amount.fraction)
+ assertEquals("500000000.00000001 EUR", amount.toString())
+
+ str = "EUR:1500000000.00000003"
+ amount = Amount.fromJSONString(str)
+ assertEquals(str, amount.toJSONString())
+ assertEquals("EUR", amount.currency)
+ assertEquals(1500000000, amount.value)
+ assertEquals(3, amount.fraction)
+ assertEquals("1500000000.00000003 EUR", amount.toString())
+ }
+
+ @Test
+ fun testFromJSONStringAcceptsMaxValuesRejectsAbove() {
+ val maxValue = 4503599627370496
+ val str = "TESTKUDOS123:$maxValue.99999999"
+ val amount = Amount.fromJSONString(str)
+ assertEquals(str, amount.toJSONString())
+ assertEquals("TESTKUDOS123", amount.currency)
+ assertEquals(maxValue, amount.value)
+ assertEquals("$maxValue.99999999 TESTKUDOS123", amount.toString())
+
+ // longer currency not accepted
+ assertThrows<AmountParserException>("longer currency was accepted") {
+ Amount.fromJSONString("TESTKUDOS1234:$maxValue.99999999")
+ }
+
+ // max value + 1 not accepted
+ assertThrows<AmountParserException>("max value + 1 was accepted") {
+ Amount.fromJSONString("TESTKUDOS123:${maxValue + 1}.99999999")
+ }
+
+ // max fraction + 1 not accepted
+ assertThrows<AmountParserException>("max fraction + 1 was accepted") {
+ Amount.fromJSONString("TESTKUDOS123:$maxValue.999999990")
+ }
+ }
+
+ @Test
+ fun testFromJSONStringRejections() {
+ assertThrows<AmountParserException> {
+ Amount.fromJSONString("TESTKUDOS:0,5")
+ }
+ assertThrows<AmountParserException> {
+ Amount.fromJSONString("+TESTKUDOS:0.5")
+ }
+ assertThrows<AmountParserException> {
+ Amount.fromJSONString("0.5")
+ }
+ assertThrows<AmountParserException> {
+ Amount.fromJSONString(":0.5")
+ }
+ assertThrows<AmountParserException> {
+ Amount.fromJSONString("EUR::0.5")
+ }
+ assertThrows<AmountParserException> {
+ Amount.fromJSONString("EUR:.5")
+ }
+ }
+
+ @Test
+ fun testAddition() {
+ assertEquals(
+ Amount.fromJSONString("EUR:2"),
+ Amount.fromJSONString("EUR:1") + Amount.fromJSONString("EUR:1")
+ )
+ assertEquals(
+ Amount.fromJSONString("EUR:3"),
+ Amount.fromJSONString("EUR:1.5") + Amount.fromJSONString("EUR:1.5")
+ )
+ assertEquals(
+ Amount.fromJSONString("EUR:500000000.00000002"),
+ Amount.fromJSONString("EUR:500000000.00000001") + Amount.fromJSONString("EUR:0.00000001")
+ )
+ assertThrows<AmountOverflowException>("addition didn't overflow") {
+ Amount.fromJSONString("EUR:4503599627370496.99999999") + Amount.fromJSONString("EUR:0.00000001")
+ }
+ assertThrows<AmountOverflowException>("addition didn't overflow") {
+ Amount.fromJSONString("EUR:4000000000000000") + Amount.fromJSONString("EUR:4000000000000000")
+ }
+ }
+
+ @Test
+ fun testTimes() {
+ assertEquals(
+ Amount.fromJSONString("EUR:2"),
+ Amount.fromJSONString("EUR:2") * 1
+ )
+ assertEquals(
+ Amount.fromJSONString("EUR:2"),
+ Amount.fromJSONString("EUR:1") * 2
+ )
+ assertEquals(
+ Amount.fromJSONString("EUR:4.5"),
+ Amount.fromJSONString("EUR:1.5") * 3
+ )
+ assertEquals(Amount.fromJSONString("EUR:0"), Amount.fromJSONString("EUR:1.11") * 0)
+ assertEquals(Amount.fromJSONString("EUR:1.11"), Amount.fromJSONString("EUR:1.11") * 1)
+ assertEquals(Amount.fromJSONString("EUR:2.22"), Amount.fromJSONString("EUR:1.11") * 2)
+ assertEquals(Amount.fromJSONString("EUR:3.33"), Amount.fromJSONString("EUR:1.11") * 3)
+ assertEquals(Amount.fromJSONString("EUR:4.44"), Amount.fromJSONString("EUR:1.11") * 4)
+ assertEquals(Amount.fromJSONString("EUR:5.55"), Amount.fromJSONString("EUR:1.11") * 5)
+ assertEquals(
+ Amount.fromJSONString("EUR:1500000000.00000003"),
+ Amount.fromJSONString("EUR:500000000.00000001") * 3
+ )
+ assertThrows<AmountOverflowException>("times didn't overflow") {
+ Amount.fromJSONString("EUR:4000000000000000") * 2
+ }
+ }
+
+ @Test
+ fun testSubtraction() {
+ assertEquals(
+ Amount.fromJSONString("EUR:0"),
+ Amount.fromJSONString("EUR:1") - Amount.fromJSONString("EUR:1")
+ )
+ assertEquals(
+ Amount.fromJSONString("EUR:1.5"),
+ Amount.fromJSONString("EUR:3") - Amount.fromJSONString("EUR:1.5")
+ )
+ assertEquals(
+ Amount.fromJSONString("EUR:500000000.00000001"),
+ Amount.fromJSONString("EUR:500000000.00000002") - Amount.fromJSONString("EUR:0.00000001")
+ )
+ assertThrows<AmountOverflowException>("subtraction didn't underflow") {
+ Amount.fromJSONString("EUR:23.42") - Amount.fromJSONString("EUR:42.23")
+ }
+ assertThrows<AmountOverflowException>("subtraction didn't underflow") {
+ Amount.fromJSONString("EUR:0.5") - Amount.fromJSONString("EUR:0.50000001")
+ }
+ }
+
+ @Test
+ fun testIsZero() {
+ assertTrue(Amount.zero("EUR").isZero())
+ assertTrue(Amount.fromJSONString("EUR:0").isZero())
+ assertTrue(Amount.fromJSONString("EUR:0.0").isZero())
+ assertTrue(Amount.fromJSONString("EUR:0.00000").isZero())
+ assertTrue((Amount.fromJSONString("EUR:1.001") - Amount.fromJSONString("EUR:1.001")).isZero())
+
+ assertFalse(Amount.fromJSONString("EUR:0.00000001").isZero())
+ assertFalse(Amount.fromJSONString("EUR:1.0").isZero())
+ assertFalse(Amount.fromJSONString("EUR:0001.0").isZero())
+ }
+
+ @Test
+ fun testComparision() {
+ assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0"))
+ assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0.00000001"))
+ assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:0.00000001"))
+ assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:1"))
+ assertEquals(Amount.fromJSONString("EUR:0"), Amount.fromJSONString("EUR:0"))
+ assertEquals(Amount.fromJSONString("EUR:42"), Amount.fromJSONString("EUR:42"))
+ assertEquals(Amount.fromJSONString("EUR:42.00000001"), Amount.fromJSONString("EUR:42.00000001"))
+ assertTrue(Amount.fromJSONString("EUR:42.00000001") >= Amount.fromJSONString("EUR:42.00000001"))
+ assertTrue(Amount.fromJSONString("EUR:42.00000002") >= Amount.fromJSONString("EUR:42.00000001"))
+ assertTrue(Amount.fromJSONString("EUR:42.00000002") > Amount.fromJSONString("EUR:42.00000001"))
+ assertTrue(Amount.fromJSONString("EUR:0.00000002") > Amount.fromJSONString("EUR:0.00000001"))
+ assertTrue(Amount.fromJSONString("EUR:0.00000001") > Amount.fromJSONString("EUR:0"))
+ assertTrue(Amount.fromJSONString("EUR:2") > Amount.fromJSONString("EUR:1"))
+
+ assertThrows<IllegalStateException>("could compare amounts with different currencies") {
+ Amount.fromJSONString("EUR:0.5") < Amount.fromJSONString("USD:0.50000001")
+ }
+ }
+
+ @Test
+ fun testToByteArray() {
+ val vectors = listOf(
+ Pair("ceicWVf9GhJ:3902026702525079.40496378", "006XSQV3G899E0K9XKX66SB9CDBNCSHS8XM4M00"),
+ Pair("asYDLuK2A:3800267550024600.02072907", "006R0MNXBVHSG00ZM55P2WTS8H67AJSJ8400000"),
+ Pair("pV1m:1347558259914570.09786232", "002CK66VCNVMM04NADW70NHHDM0000000000000"),
+ Pair("geO82l:553744321840253.41004983", "000ZF855K627T0KHNYVPESAF70S6R0000000000"),
+ Pair("B9bWK7WPEO:3663912678613976.12122563", "006G8KS5P9HXG05RZ71M4EB2AX5KENTG8N7G000"),
+ Pair("X:1537372109907438.77850768", "002QCETPFYJYW153X285G000000000000000000"),
+ Pair("5:4271492725553118.39728399", "007JSSK6J4VXW0JY6M7KA000000000000000000"),
+ Pair("OSdV:801656289790342.08256189", "001DJ6H6CA4RC03XZAYMYMV4AR0000000000000"),
+ Pair("Y6:2908617536334646.94126271", "0055AQTB19NKC1CW82ZNJDG0000000000000000"),
+ Pair("kSHoOZj:2610656582865206.00292046", "004MCR6T828KC004EK76PMT8DX7NMTG00000000"),
+ Pair("GkhLXrlGES:4246330707533398.83874252", "007HC0Z9DFF5C17ZT764ETV89HC74V278N9G000"),
+ Pair("CNS09:738124490298524.71259462", "0019YMG01DA9R11ZAN346KJK60WG00000000000"),
+ Pair("sw0b1tKXZym:2132978464977419.28199478", "003S7VNZPZS0P0DE98V76XSGC8RQ8JTRB9WPT00"),
+ Pair("fC:1275322307696988.17178522", "0028FSGX3ZCNR0863YD6CGR0000000000000000"),
+ Pair("cRai6j:166032749022734.69444771", "0009E0C30V70W113MJHP6MK1D4V6M0000000000"),
+ Pair("KOADwTb3:3932974019564218.48282023", "006ZJ16ZB39BM0Q0Q6KMPKT18HVN8RHK0000000"),
+ Pair("9Fi9wcLgDe:1268366772151214.97268853", "002834N6WRHTW1EC6HTKJHK975VP6K378HJG000"),
+ Pair("SDN:3370670470236379.88943272", "005ZK6V0124DP1AD5AM56H2E000000000000000"),
+ Pair("zGCP5V:4010014441349620.76121145", "0073Y5HYA8GZ8149GGWQMHT3A0TNC0000000000"),
+ Pair("VsW1JjBLn:2037070181191907.99717275", "003KSD2WH18E61FHJ2DNCWTQ6556MGJCDR00000"),
+ Pair("A:1806895799429502.00887758", "0036PQ5P8NMQW00DHF742000000000000000000"),
+ Pair("njA8:4015261148004966.43708687", "00747PYPD116C0MTY47PWTJ1700000000000000"),
+ Pair("Bwq:3562876074139250.28829179", "006AGTNTRWF740DQWQXM4XVH000000000000000"),
+ Pair("8e75v8:3716241006992995.95213823", "006K7SP93WF661DCV3ZKGS9Q6NV3G0000000000"),
+ Pair("XrnbQTTn:3887603772953949.94721267", "006WZGA9X8ANT1D5AKSNGWKEC98N8N3E0000000"),
+ Pair("MIN:0.00000001", "0000000000000000000MTJAE000000000000000"),
+ Pair("MAX:4503599627370496.99999999", "00800000000001FNW3ZMTGAR000000000000000")
+ )
+ for (v in vectors) {
+ val amount = Amount.fromJSONString(v.first)
+ val encodedBytes = Base32Crockford.encode(amount.toByteArray())
+ assertEquals(v.second, encodedBytes)
+ }
+ }
+
+ private inline fun <reified T : Throwable> assertThrows(
+ msg: String? = null,
+ function: () -> Any
+ ) {
+ try {
+ function.invoke()
+ fail(msg)
+ } catch (e: Exception) {
+ assertTrue(e is T)
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/Base32CrockfordTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/Base32CrockfordTest.kt
new file mode 100644
index 0000000..565a395
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/Base32CrockfordTest.kt
@@ -0,0 +1,123 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+@ExperimentalStdlibApi
+class Base32CrockfordTest {
+
+ private class TestVector(val value: ByteArray, val encoding: List<String>)
+
+ private val vectors = listOf(
+ TestVector(byteArrayOf(0), listOf("00", "0O", "0o")),
+ TestVector(byteArrayOf(0), listOf("00", "0O", "0o")),
+ TestVector(byteArrayOf(1), listOf("04")),
+ TestVector(byteArrayOf(2), listOf("08")),
+ TestVector(byteArrayOf(3), listOf("0C")),
+ TestVector(byteArrayOf(4), listOf("0G")),
+ TestVector(byteArrayOf(5), listOf("0M")),
+ TestVector(byteArrayOf(6), listOf("0R")),
+ TestVector(byteArrayOf(7), listOf("0W")),
+ TestVector(byteArrayOf(8), listOf("10")),
+ TestVector(byteArrayOf(9), listOf("14")),
+ TestVector(byteArrayOf(10), listOf("18")),
+ TestVector(byteArrayOf(11), listOf("1C")),
+ TestVector(byteArrayOf(12), listOf("1G")),
+ TestVector(byteArrayOf(13), listOf("1M")),
+ TestVector(byteArrayOf(14), listOf("1R")),
+ TestVector(byteArrayOf(15), listOf("1W")),
+ TestVector(byteArrayOf(16), listOf("20")),
+ TestVector(byteArrayOf(17), listOf("24")),
+ TestVector(byteArrayOf(18), listOf("28")),
+ TestVector(byteArrayOf(19), listOf("2C")),
+ TestVector(byteArrayOf(20), listOf("2G")),
+ TestVector(byteArrayOf(21), listOf("2M")),
+ TestVector(byteArrayOf(22), listOf("2R")),
+ TestVector(byteArrayOf(23), listOf("2W")),
+ TestVector(byteArrayOf(24), listOf("30")),
+ TestVector(byteArrayOf(25), listOf("34")),
+ TestVector(byteArrayOf(26), listOf("38")),
+ TestVector(byteArrayOf(27), listOf("3C")),
+ TestVector(byteArrayOf(28), listOf("3G")),
+ TestVector(byteArrayOf(29), listOf("3M")),
+ TestVector(byteArrayOf(30), listOf("3R")),
+ TestVector(byteArrayOf(31), listOf("3W")),
+ TestVector(byteArrayOf(0, 0), listOf("0000", "oooo", "OOOO", "0oO0")),
+ TestVector(byteArrayOf(1, 0), listOf("0400", "o4oo", "O4OO", "04oO")),
+ TestVector(byteArrayOf(0, 1), listOf("000G", "ooog", "OOOG", "0oOg")),
+ TestVector(byteArrayOf(1, 1), listOf("040G", "o4og", "O4og", "04Og")),
+ TestVector(byteArrayOf(136.toByte(), 64), listOf("H100", "hio0", "HLOo")),
+ TestVector(byteArrayOf(139.toByte(), 188.toByte()), listOf("HEY0", "heyo", "HeYO")),
+ TestVector(byteArrayOf(54, 31, 127), listOf("6RFQY", "6rfqy")),
+ TestVector(
+ byteArrayOf(72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33),
+ listOf("91JPRV3F41BPYWKCCGGG", "91jprv3f41bpywkccggg", "9Ljprv3f4ibpywkccggg")
+ ),
+ TestVector(
+ byteArrayOf(139.toByte(), 130.toByte(), 16, 112, 24, 11, 64),
+ listOf("HE110W0R1D00", "helloworld00", "heiiOw0RidoO")
+ ),
+ TestVector(
+ byteArrayOf(139.toByte(), 130.toByte(), 16, 112, 24, 11),
+ listOf("HE110W0R1C", "helloworlc", "heiiOw0RiC")
+ ),
+ TestVector(
+ byteArrayOf(139.toByte(), 130.toByte(), 16, 112, 24, 11, 0),
+ listOf("HE110W0R1C00", "helloworlc00", "heiiOw0RiC00")
+ )
+ )
+
+ @Test
+ fun testEncode() {
+ for (vector in vectors) {
+ assertEquals(vector.encoding[0], Base32Crockford.encode(vector.value))
+ }
+ }
+
+ @Test
+ fun testDecode() {
+ for (vector in vectors) {
+ for (encoding in vector.encoding) {
+ assertTrue(vector.value contentEquals Base32Crockford.decode(encoding))
+ }
+ }
+ }
+
+ @Ignore // TODO
+ @Test
+ fun testDecodeFuck() {
+ val bytes = byteArrayOf(0x7e, 0xd9.toByte())
+ assertTrue(bytes contentEquals Base32Crockford.decode("FUCK"))
+ assertTrue(bytes contentEquals Base32Crockford.decode("FuCk"))
+ assertTrue(bytes contentEquals Base32Crockford.decode("fUcK"))
+ assertTrue(bytes contentEquals Base32Crockford.decode("FVCK"))
+ }
+
+ @Test
+ fun testEncodingDecoding() {
+ val input = "Hello, World"
+ val encoded = Base32Crockford.encode(input.encodeToByteArray())
+ val decoded = Base32Crockford.decode(encoded)
+ val output = decoded.decodeToString()
+ assertEquals(input, output)
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt
new file mode 100644
index 0000000..ab4770d
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt
@@ -0,0 +1,100 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import net.taler.wallet.kotlin.exchange.Denominations.denomination10
+import net.taler.wallet.kotlin.exchange.Denominations.denomination5
+import net.taler.wallet.kotlin.exchange.DenominationStatus.Unverified
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedGood
+import net.taler.wallet.kotlin.exchange.ExchangeRecord
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateReason.Initial
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateStatus.FetchKeys
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class DbTest {
+
+ private val dbFactory = DbFactory()
+
+ private val exchange1 = ExchangeRecord(
+ baseUrl = "https://example1.org/",
+ timestampAdded = Timestamp.now(),
+ updateStatus = FetchKeys,
+ updateStarted = Timestamp.now(),
+ updateReason = Initial
+ )
+ private val exchange2 = ExchangeRecord(
+ baseUrl = "https://example2.org/",
+ timestampAdded = Timestamp.now(),
+ updateStatus = FetchKeys,
+ updateStarted = Timestamp.now(),
+ updateReason = Initial
+ )
+
+ @Test
+ fun testExchanges() = runCoroutine {
+ val db = dbFactory.openDb()
+ var exchanges = db.listExchanges()
+ assertEquals(0, exchanges.size)
+
+ db.put(exchange1)
+ exchanges = db.listExchanges()
+ assertEquals(1, exchanges.size)
+ assertEquals(exchange1, exchanges[0])
+
+ db.put(exchange2)
+ exchanges = db.listExchanges()
+ assertEquals(2, exchanges.size)
+ assertEquals(exchange1, db.getExchangeByBaseUrl(exchange1.baseUrl))
+ assertEquals(exchange2, db.getExchangeByBaseUrl(exchange2.baseUrl))
+
+ db.deleteExchangeByBaseUrl(exchange1.baseUrl)
+ exchanges = db.listExchanges()
+ assertEquals(1, exchanges.size)
+ assertEquals(exchange2, exchanges[0])
+ }
+
+ @Test
+ fun testDenominations() = runCoroutine {
+ val db = dbFactory.openDb()
+ val url = denomination10.exchangeBaseUrl
+
+ // no denominations should be in DB
+ var denominations = db.getDenominationsByBaseUrl(url)
+ assertEquals(0, denominations.size)
+
+ // add a denomination and check that it is really there
+ db.put(denomination10)
+ denominations = db.getDenominationsByBaseUrl(url)
+ assertEquals(1, denominations.size)
+ assertEquals(denomination10, denominations[0])
+
+ // modify existing denomination and check that it gets updated
+ assertEquals(Unverified, denomination10.status)
+ val newDenomination = denomination10.copy(status = VerifiedGood)
+ db.put(newDenomination)
+ denominations = db.getDenominationsByBaseUrl(url)
+ assertEquals(1, denominations.size)
+ assertEquals(newDenomination, denominations[0])
+
+ // add one more denomination
+ db.put(denomination5)
+ denominations = db.getDenominationsByBaseUrl(url)
+ assertEquals(2, denominations.size)
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/PaytoUriTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/PaytoUriTest.kt
new file mode 100644
index 0000000..4f080e3
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/PaytoUriTest.kt
@@ -0,0 +1,58 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+
+class PaytoUriTest {
+
+ @Test
+ fun testFromString() {
+ // wrong scheme
+ var uri = "https://example.com/"
+ assertNull(PaytoUri.fromString(uri))
+
+ // incomplete scheme
+ uri = "payto:blabla"
+ assertNull(PaytoUri.fromString(uri))
+
+ // proper URI
+ uri = "payto://x-taler-bank/123"
+ var parsedUri = PaytoUri.fromString(uri)
+ assertNotNull(parsedUri)
+ assertEquals("x-taler-bank", parsedUri.targetType)
+ assertEquals("123", parsedUri.targetPath)
+
+ // proper URI with incomplete query
+ uri = "payto://x-taler-bank/123?foo"
+ parsedUri = PaytoUri.fromString(uri)
+ assertNotNull(parsedUri)
+ assertEquals(0, parsedUri.params.size)
+
+ // proper URI with two query param
+ uri = "payto://x-taler-bank/123?foo=bar&hip=hop"
+ parsedUri = PaytoUri.fromString(uri)
+ assertNotNull(parsedUri)
+ assertEquals(2, parsedUri.params.size)
+ assertEquals("bar", parsedUri.params["foo"])
+ assertEquals("hop", parsedUri.params["hip"])
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/TalerUriTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/TalerUriTest.kt
new file mode 100644
index 0000000..cfcc8bd
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/TalerUriTest.kt
@@ -0,0 +1,65 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import net.taler.wallet.kotlin.TalerUri.WithdrawUriResult
+import net.taler.wallet.kotlin.TalerUri.parseWithdrawUri
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+class TalerUriTest {
+
+ @Test
+ fun testParseWithdrawUri() {
+ // correct parsing
+ var uri = "taler://withdraw/bank.example.com/12345"
+ var expected = WithdrawUriResult("https://bank.example.com/", "12345")
+ assertEquals(expected, parseWithdrawUri(uri))
+
+ // correct parsing with insecure http
+ uri = "taler+http://withdraw/bank.example.org/foo"
+ expected = WithdrawUriResult("http://bank.example.org/", "foo")
+ assertEquals(expected, parseWithdrawUri(uri))
+
+ // correct parsing with long path
+ uri = "taler://withdraw/bank.example.com/foo/bar/23/42/1337/1234567890"
+ expected = WithdrawUriResult("https://bank.example.com/foo/bar/23/42/1337", "1234567890")
+ assertEquals(expected, parseWithdrawUri(uri))
+
+ // rejects incorrect scheme
+ uri = "talerx://withdraw/bank.example.com/12345"
+ assertNull(parseWithdrawUri(uri))
+
+ // rejects incorrect authority
+ uri = "taler://withdrawx/bank.example.com/12345"
+ assertNull(parseWithdrawUri(uri))
+
+ // rejects incorrect authority with insecure http
+ uri = "taler+http://withdrawx/bank.example.com/12345"
+ assertNull(parseWithdrawUri(uri))
+
+ // rejects empty withdrawalId
+ uri = "taler://withdraw/bank.example.com//"
+ assertNull(parseWithdrawUri(uri))
+
+ // rejects empty path and withdrawalId
+ uri = "taler://withdraw/bank.example.com////"
+ assertNull(parseWithdrawUri(uri))
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
new file mode 100644
index 0000000..0ece68e
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
@@ -0,0 +1,69 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.mock.MockEngine
+import io.ktor.client.engine.mock.MockEngineConfig
+import io.ktor.client.engine.mock.respond
+import io.ktor.client.features.json.JsonFeature
+import io.ktor.client.features.json.serializer.KotlinxSerializer
+import io.ktor.http.ContentType.Application
+import io.ktor.http.Url
+import io.ktor.http.fullPath
+import io.ktor.http.headersOf
+import io.ktor.http.hostWithPort
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.serialization.json.Json
+
+enum class PlatformTarget {
+ ANDROID,
+ JS,
+ NATIVE,
+}
+
+/**
+ * Workaround to use suspending functions in unit tests
+ */
+expect fun runCoroutine(block: suspend (scope: CoroutineScope) -> Unit)
+
+expect fun getPlatformTarget(): PlatformTarget
+
+fun getMockHttpClient(): HttpClient = HttpClient(MockEngine) {
+ install(JsonFeature) {
+ serializer = KotlinxSerializer(Json(getJsonConfiguration()))
+ }
+ engine {
+ addHandler { error("No test handler added") }
+ }
+}
+
+fun HttpClient.giveJsonResponse(url: String, jsonProducer: () -> String) {
+ val httpConfig = engineConfig as MockEngineConfig
+ httpConfig.requestHandlers.removeAt(0)
+ httpConfig.requestHandlers.add { request ->
+ if (request.url.fullUrl == url) {
+ val headers = headersOf("Content-Type" to listOf(Application.Json.toString()))
+ respond(jsonProducer(), headers = headers)
+ } else {
+ error("Unexpected URL: ${request.url.fullUrl}")
+ }
+ }
+}
+
+private val Url.hostWithPortIfRequired: String get() = if (port == protocol.defaultPort) host else hostWithPort
+private val Url.fullUrl: String get() = "${protocol.name}://$hostWithPortIfRequired$fullPath"
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/TimestampTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/TimestampTest.kt
new file mode 100644
index 0000000..1a12549
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/TimestampTest.kt
@@ -0,0 +1,75 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class TimestampTest {
+
+ companion object {
+ fun getRandomTimestamp() = Timestamp(Random.nextLong(0, 9007199254740991))
+ }
+
+ @Test
+ fun testRoundedToByteArray() {
+ val vectors = listOf<Pair<Long, String>>(
+ Pair(0, "0000000000000"),
+ Pair(23, "0000000000000"),
+ Pair(3349, "000000005Q3C0"),
+ Pair(61227, "00000003MB4M0"),
+ Pair(143879, "00000008GR0W0"),
+ Pair(8879237, "000000GH7B4W0"),
+ Pair(16058145, "000000XX46H80"),
+ Pair(270909464, "00000FRKDX4M0"),
+ Pair(5500325225, "0000A054XBSM0"),
+ Pair(52631835363, "0002ZQJDTGYC0"),
+ Pair(567067373675, "00107FN9AKAM0"),
+ Pair(1036693403335, "001TXQFY0VEC0"),
+ Pair(88636710366804, "04XED7JKDJSR0"),
+ Pair(852207301364437, "1F9TC1M0SEJG0"),
+ Pair(8312646819781097, "EDE78FC4AEXM0"),
+ Pair(7473472692572260, "CYVHQMAQAR7G0"),
+ Pair(1148188526507363, "1ZQJYRD9M40C0"),
+ Pair(5418115526173127, "9CRG6QASJ80M0"),
+ Pair(4046218176532046, "70KGVZK7XCPG0"),
+ Pair(2421361923399585, "46D6FNS4VAFW0"),
+ Pair(7305555710693483, "CNH8RDJYNV1M0"),
+ Pair(2857858080018042, "4YMJDJ1XYFM80"),
+ Pair(2037218281967033, "3H2TEEYTJCCW0"),
+ Pair(7912348432268295, "DQ74XYJCEFXG0"),
+ Pair(6416777738213721, "B46FJPQRT81M0"),
+ Pair(6914097778740296, "BZSWYK0W3NTG0"),
+ Pair(7090360690428000, "C9K0AYTABAVG0"),
+ Pair(1998560202566445, "3EY4ZTJR5QER0"),
+ Pair(7896179665141956, "DPADV4EQCHKM0"),
+ Pair(6266851558322330, "AVW58JG1WB880"),
+ Pair(1878397422799871, "388PH07WY68W0"),
+ Pair(3767372253320333, "6H46A6MZSMS00"),
+ Pair(8065344266359580, "DZPXQR6KHZ5W0"),
+ Pair(7947440620360995, "DS5FP8RA25H00"),
+ Pair(3414000286898485, "5XGFEB1VMC880"),
+ Pair(9007199254740991, "FKZZZZZZY3EG0")
+ ) // TODO add more test vectors beyond 9007199254740991 (typescript max of wallet-core)
+ for (v in vectors) {
+ val encodedBytes = Base32Crockford.encode(Timestamp(v.first).roundedToByteArray())
+ assertEquals(v.second, encodedBytes)
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/VersionTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/VersionTest.kt
new file mode 100644
index 0000000..d445ebc
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/VersionTest.kt
@@ -0,0 +1,57 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+class VersionTest {
+
+ @Test
+ fun testComparision() {
+ assertNull(compareVersions("0:0:0", ""))
+ assertNull(compareVersions("", "0:0:0"))
+ assertNull(compareVersions("foo", "0:0:0"))
+ assertNull(compareVersions("0:0:0", "foo"))
+ assertEquals(
+ VersionMatchResult(true, 0),
+ compareVersions("0:0:0", "0:0:0")
+ )
+ assertEquals(
+ VersionMatchResult(true, -1),
+ compareVersions("0:0:0", "1:0:1")
+ )
+ assertEquals(
+ VersionMatchResult(true, -1),
+ compareVersions("0:0:0", "1:5:1")
+ )
+ assertEquals(
+ VersionMatchResult(false, -1),
+ compareVersions("0:0:0", "1:5:0")
+ )
+ assertEquals(
+ VersionMatchResult(false, 1),
+ compareVersions("1:0:0", "0:5:0")
+ )
+ assertEquals(
+ VersionMatchResult(true, 0),
+ compareVersions("1:0:1", "1:5:1")
+ )
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/WalletApiTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/WalletApiTest.kt
new file mode 100644
index 0000000..7971be5
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/WalletApiTest.kt
@@ -0,0 +1,93 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+@Ignore // Live-Test which fails when test environment changes or is not available
+class WalletApiTest {
+
+ private val api = WalletApi()
+
+ private val exchangeBaseUrl = "https://exchange.test.taler.net/"
+ private val withdrawUri =
+ "taler://withdraw/bank.test.taler.net/8adabbf8-fe61-4d31-a2d1-89350b5be8fa"
+ private val paytoUris = listOf("payto://x-taler-bank/bank.test.taler.net/Exchange")
+ private val tosEtag = "0"
+
+ @Test
+ fun testGetVersions() {
+ api.getVersions()
+ }
+
+ @Test
+ fun testGetWithdrawalDetails() = runCoroutine {
+ val detailsForUri = api.getWithdrawalDetailsForUri(withdrawUri)
+ assertEquals(Amount("TESTKUDOS", 5, 0), detailsForUri.amount)
+ assertEquals(exchangeBaseUrl, detailsForUri.defaultExchangeBaseUrl)
+
+ val detailsForAmount = api.getWithdrawalDetailsForAmount(
+ detailsForUri.defaultExchangeBaseUrl!!,
+ detailsForUri.amount
+ )
+ assertEquals(Amount("TESTKUDOS", 5, 0), detailsForAmount.amountRaw)
+ assertEquals(Amount("TESTKUDOS", 4, 80000000), detailsForAmount.amountEffective)
+ assertEquals(false, detailsForAmount.tosAccepted)
+ }
+
+ @Test
+ fun testExchangeManagement() = runCoroutine {
+ // initially we have no exchanges
+ assertTrue(api.listExchanges().isEmpty())
+
+ // test exchange gets added correctly
+ val exchange = api.addExchange(exchangeBaseUrl)
+ assertEquals(exchangeBaseUrl, exchange.exchangeBaseUrl)
+ assertEquals("TESTKUDOS", exchange.currency)
+ assertEquals(paytoUris, exchange.paytoUris)
+
+ // added exchange got added to list
+ val exchanges = api.listExchanges()
+ assertEquals(1, exchanges.size)
+ assertEquals(exchange, exchanges[0])
+
+ // ToS are not accepted
+ val tosResult = api.getExchangeTos(exchangeBaseUrl)
+ assertEquals(null, tosResult.acceptedEtag)
+ assertEquals(tosEtag, tosResult.currentEtag)
+ assertTrue(tosResult.tos.length > 100)
+
+ // withdrawal details also show ToS as not accepted
+ val withdrawalDetails =
+ api.getWithdrawalDetailsForAmount(exchangeBaseUrl, Amount.zero("TESTKUDOS"))
+ assertEquals(false, withdrawalDetails.tosAccepted)
+
+ // accept ToS
+ api.setExchangeTosAccepted(exchangeBaseUrl, tosResult.currentEtag)
+ val newTosResult = api.getExchangeTos(exchangeBaseUrl)
+ assertEquals(newTosResult.currentEtag, newTosResult.acceptedEtag)
+
+ // withdrawal details now show ToS as accepted as well
+ val newWithdrawalDetails =
+ api.getWithdrawalDetailsForAmount(exchangeBaseUrl, Amount.zero("TESTKUDOS"))
+ assertEquals(true, newWithdrawalDetails.tosAccepted)
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/DepositTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/DepositTest.kt
new file mode 100644
index 0000000..399b754
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/DepositTest.kt
@@ -0,0 +1,90 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.crypto.Deposit.CoinDepositPermission
+import net.taler.wallet.kotlin.crypto.Deposit.DepositInfo
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class DepositTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+ private val deposit = Deposit(crypto)
+
+ private class DepositVector(val depositInfo: DepositInfo, val permission: CoinDepositPermission)
+
+ @Test
+ fun test() {
+ val vectors = listOf(
+ DepositVector(
+ DepositInfo(
+ exchangeBaseUrl = "example.org",
+ contractTermsHash = "QS3RK40J3262QRAJG4SVZB0C60N45W7D71ZXFTP385E05PKR7RGH22QTPA109Z0EBD120SHA09WHA72K4PCFM6JTFMCZEGVW2T9FT2R",
+ coinPublicKey = "6DMP3NPY6DZD0QDMEW6955R6ZCX3T052PRJE0FYHTKRA837VMQT0",
+ coinPrivateKey = "XWVF454Q600NGSMF7RYXY369N2DJKC1YQZG0RFAF8EMRRY8DJE50",
+ spendAmount = Amount("TESTKUDOS", 8, 0),
+ timestamp = Timestamp(1593522039000),
+ refundDeadline = Timestamp(1593522939000),
+ merchantPublicKey = "RQSR35MJJDP5Y27XMBEV7QBG4HTE3JXR1JCE4NCYCAQHKJ8CB5T0",
+ feeDeposit = Amount("TESTKUDOS", 0, 2000000),
+ wireInfoHash = "5C3WXB9PVPXAGGQVRJGAMBJJQBBYPHZEZZYVHQ8RS97F9EZABPAWNWSGD1VH6YNFB58GPJWX1A17DFNCK0S9YPP7PED3ZXR76E2A0XR",
+ denomPublicKey = "020000YQX8JZNBMJ8PTVRZZT27TTSJN5HM0037K0BSY3YWP0VF8GFYYAN06J0PTQVQGQJGVJ91CHWD6QCH9MKADR1VH31P5WEMTMWWTX6KWA3KX0FD1RWQSPJ8W8ZXVWPVDJVVJ08W12VWV39A9W4SDCMWBSQ17QV4RP86JJFMTEWB540KKYHY3ZYJEQYVN9XD7CM7SCAMM8WV03P0VG2001",
+ denomSignature = "9F3M4AZ8BN3MJRH6T49TD9R4EVTF6C5NH365DHCC73F39N3SAK422NY9ZWN7WANGW3M4XZTJDDV1B1E7MSW03VCESKSF8889EDTRE1VHF0FT3E5CT0Q449JAQQ6DSDY4D9JMPP99TRKZX86VAXN45FBSBXTJZ2FN85Y67T9ADDNDXMV060J0HP7G5YXXJQ0V7KHACEZFVXFH6"
+ ),
+ CoinDepositPermission(
+ coinSignature = "CZX4HMBR6H3W1KAN1T0HZ11VBSERZ6JM715MKSDAJJSXWQTZ0GDCZX92MAKMH0DB7B389E24XGP6X7ZSAQVW540KTQZJ9RMR0R9CM08",
+ coinPublicKey = "6DMP3NPY6DZD0QDMEW6955R6ZCX3T052PRJE0FYHTKRA837VMQT0",
+ denomSignature = "9F3M4AZ8BN3MJRH6T49TD9R4EVTF6C5NH365DHCC73F39N3SAK422NY9ZWN7WANGW3M4XZTJDDV1B1E7MSW03VCESKSF8889EDTRE1VHF0FT3E5CT0Q449JAQQ6DSDY4D9JMPP99TRKZX86VAXN45FBSBXTJZ2FN85Y67T9ADDNDXMV060J0HP7G5YXXJQ0V7KHACEZFVXFH6",
+ denomPublicKey = "020000YQX8JZNBMJ8PTVRZZT27TTSJN5HM0037K0BSY3YWP0VF8GFYYAN06J0PTQVQGQJGVJ91CHWD6QCH9MKADR1VH31P5WEMTMWWTX6KWA3KX0FD1RWQSPJ8W8ZXVWPVDJVVJ08W12VWV39A9W4SDCMWBSQ17QV4RP86JJFMTEWB540KKYHY3ZYJEQYVN9XD7CM7SCAMM8WV03P0VG2001",
+ contribution = "TESTKUDOS:8",
+ exchangeBaseUrl = "example.org"
+ )
+ ),
+ DepositVector(
+ DepositInfo(
+ exchangeBaseUrl = "example.org",
+ contractTermsHash = "CWWDVCEX745A092KB3W7K98M7EVK4G5HJRHKR0RTPKAFR1VSK147ER131PT23P8ZWH2VMWAWENTVTAXP4KDRQ9YY0951N2G2JZFGEXG",
+ coinPublicKey = "ZR6BER43XSR3NK705HFKGC842Q3Q2R4G3T6VQ5JEK8EAC34JYW6G",
+ coinPrivateKey = "CP8WW83V239DTXD84M87TZ0DG1TGM4RC3D77NN936554B7GDVBRG",
+ spendAmount = Amount("TESTKUDOS", 1, 0),
+ timestamp = Timestamp(1593522635000),
+ refundDeadline = Timestamp(1593523535000),
+ merchantPublicKey = "S8WFGCK6CJGFWYWHAY56NTZTD84S31TGM244GTVKTCETKW5HDBQG",
+ feeDeposit = Amount("TESTKUDOS", 0, 2000000),
+ wireInfoHash = "8GQFCMQXHHTF7VG73WVJN8PFCPH4Y0ZRSWAQ01A6A9F0Y0HY59H3RDPMBPVNJJDJP1S3E8JJVE7MGAC9YDACWAVCTE75QZ8ZRNAH3X8",
+ denomPublicKey = "020000X1NCWC14MNXTAJ17W6HY5AMGXKGV392BESTESJJ8TQ41W9W2RBN69Q3WQYQQXS7KS5ZMDSSGHY0H7921X0RRA6ZNW2RSKJGXJNQM66KRDEFTQ50B6ZK60CSCY3KZ0RSGAYBG8VHC2A87Z6DQS361G8BAJS937J9YX89MGVMG896MKVEZ3H3NGRJTT89QNGN5KZWFSE6G5129GG2001",
+ denomSignature = "7XJDMABJHV01GX7S764ZY3XR3AX2KAK4AXWKZJHBRV8BBQ7KQ5FHC372GE8RF8M2NT128G85RWW87CNNVYNWWPEPSFN8QXTA2H6SGTR0EFRDF4CXAJRXHWPB450YMJM7MNNPJKXDDXCFN87RSHFZ4ESH06S0SBX6185DX2HD6JWQ3BESCK8PYCB6A09KP5ZD0EZSQKDGNGMEG"
+ ),
+ CoinDepositPermission(
+ coinSignature = "A8N1NSMSPZ4H1VR1YANYMAE74VAJ7W88EMRNDY74YXPK2WFEEHHM28VVQ3HAK6J0P9YX61XGNHRP08AEX59M0YGJ7AW8ZG2Y5FQD23G",
+ coinPublicKey = "ZR6BER43XSR3NK705HFKGC842Q3Q2R4G3T6VQ5JEK8EAC34JYW6G",
+ denomSignature = "7XJDMABJHV01GX7S764ZY3XR3AX2KAK4AXWKZJHBRV8BBQ7KQ5FHC372GE8RF8M2NT128G85RWW87CNNVYNWWPEPSFN8QXTA2H6SGTR0EFRDF4CXAJRXHWPB450YMJM7MNNPJKXDDXCFN87RSHFZ4ESH06S0SBX6185DX2HD6JWQ3BESCK8PYCB6A09KP5ZD0EZSQKDGNGMEG",
+ denomPublicKey = "020000X1NCWC14MNXTAJ17W6HY5AMGXKGV392BESTESJJ8TQ41W9W2RBN69Q3WQYQQXS7KS5ZMDSSGHY0H7921X0RRA6ZNW2RSKJGXJNQM66KRDEFTQ50B6ZK60CSCY3KZ0RSGAYBG8VHC2A87Z6DQS361G8BAJS937J9YX89MGVMG896MKVEZ3H3NGRJTT89QNGN5KZWFSE6G5129GG2001",
+ contribution = "TESTKUDOS:1",
+ exchangeBaseUrl = "example.org"
+ )
+ )
+ )
+ for (v in vectors) {
+ assertEquals(v.permission, deposit.signDepositPermission(v.depositInfo))
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/EllipticCurveTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/EllipticCurveTest.kt
new file mode 100644
index 0000000..4e83b47
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/EllipticCurveTest.kt
@@ -0,0 +1,156 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Base32Crockford
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class EllipticCurveTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+
+ @Test
+ fun testExchangeTvgEddsaKey() {
+ val pri = "9TM70AKDTS57AWY9JK2J4TMBTMW6K62WHHGZWYDG0VM5ABPZKD40"
+ val pub = "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0"
+ val pubBytes = crypto.eddsaGetPublic(Base32Crockford.decode(pri))
+ assertEquals(pub, Base32Crockford.encode(pubBytes))
+
+ val pri2 = "C9C5X5JS2SWRTSX5AQBPFA4S2DFD6V9Q04XTTEZDQ5XSSQK2P39G"
+ val pub2 = "6WC3MPYM5XKPKRA2Z6SYB81CPFV3E7EC6S2GVE095X8XH63QTZCG"
+ val pub2Bytes = crypto.eddsaGetPublic(Base32Crockford.decode(pri2))
+ assertEquals(pub2, Base32Crockford.encode(pub2Bytes))
+ }
+
+ @Test
+ fun testExchangeTvgEcdheKey() {
+ val pri = "X4T4N0M8PVQXQEBW2BA7049KFSM7J437NSDFC6GDNM3N5J9367A0"
+ val pub = "M997P494MS6A95G1P0QYWW2VNPSHSX5Q6JBY5B9YMNYWP0B50X3G"
+ val pubBytes = crypto.ecdheGetPublic(Base32Crockford.decode(pri))
+ assertEquals(pub, Base32Crockford.encode(pubBytes))
+
+ val pri2 = "MB8ZSQFTVPX4V0MGT3BA1PQS12NJ7KH33DA8D22NCWTNA5BHH2YG"
+ val pub2 = "8T2CN1W8G3XZE9C9158A5ASS74117GK1XQ1XAX5SGBFDGHS8H5W0"
+ val pub2Bytes = crypto.ecdheGetPublic(Base32Crockford.decode(pri2))
+ assertEquals(pub2, Base32Crockford.encode(pub2Bytes))
+ }
+
+ @Test
+ fun testCreateEddsaKeyPair() {
+ val pair1 = crypto.createEddsaKeyPair()
+ val pair2 = crypto.createEddsaKeyPair()
+ assertFalse(pair1.privateKey contentEquals pair2.privateKey)
+ assertFalse(pair1.publicKey contentEquals pair2.publicKey)
+ }
+
+ @Test
+ fun testCreateEcdheKeyPair() {
+ val pair1 = crypto.createEcdheKeyPair()
+ val pair2 = crypto.createEcdheKeyPair()
+ assertFalse(pair1.privateKey contentEquals pair2.privateKey)
+ assertFalse(pair1.publicKey contentEquals pair2.publicKey)
+ }
+
+ @Test
+ @ExperimentalStdlibApi
+ fun testEddsaSignAndVerify() {
+ val msg = "Hallo world!".encodeToByteArray()
+ val pri = "9TM70AKDTS57AWY9JK2J4TMBTMW6K62WHHGZWYDG0VM5ABPZKD40"
+ val expectedSig =
+ "Z6H76JXPJFP3JBGSF54XBF0BVXDJ0CJBK4YT9GVR1AT916ZD57KP53YZN5G67A4YN95WGMZKQW7744483P5JDF06B6S7TMK195QGP20"
+ val sig = crypto.eddsaSign(msg, Base32Crockford.decode(pri))
+ assertEquals(expectedSig, Base32Crockford.encode(sig))
+
+ val pub = crypto.eddsaGetPublic(Base32Crockford.decode(pri))
+ assertTrue(crypto.eddsaVerify(msg, sig, pub))
+
+ val wrongSig = Random.nextBytes(64)
+ assertFalse(crypto.eddsaVerify(msg, wrongSig, pub))
+
+ val wrongPub = Random.nextBytes(32)
+ assertFalse(crypto.eddsaVerify(msg, sig, wrongPub))
+
+ val wrongMsg = Random.nextBytes(16)
+ assertFalse(crypto.eddsaVerify(wrongMsg, sig, pub))
+
+ val pri2 = "T4NK2VVZZ1ZF6EBGEQHRK7KY67PCEVAZC5YHYH612XG5R6NJXSB0"
+ val pub2 = "2X3PSPT7D6TEM97R98C0DHZREFVAVA3XTH11D5A2Z2K7GBKQ7AEG"
+ val data2 =
+ "00000J00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+ val expectedSig2 =
+ "RFC59ZWJB88TD03F4GTQD3RWKH3KKRVEY2T1VN7CFEF51SCZ4BX8HGHC708GW1SN7Y4XFEDE3A5PFZEJ7PR7V09YZXX10DM608T1W20"
+ val sig2 = crypto.eddsaSign(Base32Crockford.decode(data2), Base32Crockford.decode(pri2))
+ assertEquals(expectedSig2, Base32Crockford.encode(sig2))
+ assertTrue(crypto.eddsaVerify(Base32Crockford.decode(data2), sig2, Base32Crockford.decode(pub2)))
+ }
+
+ @Test
+ fun testExchangeTvgEddsaEcdh() {
+ val ecdhePrivateKey = "4AFZWMSGTVCHZPQ0R81NWXDCK4N58G7SDBBE5KXE080Y50370JJG"
+ val ecdhePublicKey = "FXFN5GPAFTKVPWJDPVXQ87167S8T82T5ZV8CDYC0NH2AE14X0M30"
+ val ecdhePublicKeyBytes = crypto.ecdheGetPublic(Base32Crockford.decode(ecdhePrivateKey))
+ assertEquals(ecdhePublicKey, Base32Crockford.encode(ecdhePublicKeyBytes))
+
+ val eddsaPrivateKey = "1KG54M8T3X8BSFSZXCR3SQBSR7Y9P53NX61M864S7TEVMJ2XVPF0"
+ val eddsaPublicKey = "7BXWKG6N224C57RTDV8XEAHR108HG78NMA995BE8QAT5GC1S7E80"
+ val eddsaPublicKeyBytes = crypto.eddsaGetPublic(Base32Crockford.decode(eddsaPrivateKey))
+ assertEquals(eddsaPublicKey, Base32Crockford.encode(eddsaPublicKeyBytes))
+
+ val keyMaterial =
+ "PKZ42Z56SVK2796HG1QYBRJ6ZQM2T9QGA3JA4AAZ8G7CWK9FPX175Q9JE5P0ZAX3HWWPHAQV4DPCK10R9X3SAXHRV0WF06BHEC2ZTKR"
+ val keyMaterial1Bytes = crypto.keyExchangeEddsaEcdhe(
+ Base32Crockford.decode(eddsaPrivateKey),
+ Base32Crockford.decode(ecdhePublicKey)
+ )
+ assertEquals(keyMaterial, Base32Crockford.encode(keyMaterial1Bytes))
+
+ val keyMaterial2Bytes = crypto.keyExchangeEcdheEddsa(
+ Base32Crockford.decode(ecdhePrivateKey),
+ Base32Crockford.decode(eddsaPublicKey)
+ )
+ assertEquals(keyMaterial, Base32Crockford.encode(keyMaterial2Bytes))
+
+ val ecdhePrivateKey2 = "3AMRJ1KC87VWX9MVQMW8MB9YVX5DMS6P5V2SYZNCZ44XVMSDVEFG"
+ val ecdhePublicKey2 = "CFFVCRFTNP7701KZC7187BC42MGSVCMBWK38F23E7T9VVK9D41Q0"
+ val ecdhePublicKeyBytes2 = crypto.ecdheGetPublic(Base32Crockford.decode(ecdhePrivateKey2))
+ assertEquals(ecdhePublicKey2, Base32Crockford.encode(ecdhePublicKeyBytes2))
+
+ val eddsaPrivateKey2 = "8AZTVPCXNCVBKJVQ6XS7Z38KB9JB38WJ7Z80F9FTCX1WHZACTXJ0"
+ val eddsaPublicKey2 = "T3F1VYAVZX3DVCNJTB3BNQTRZGXN446QP2VD847CB6N5V75RT2B0"
+ val eddsaPublicKeyBytes2 = crypto.eddsaGetPublic(Base32Crockford.decode(eddsaPrivateKey2))
+ assertEquals(eddsaPublicKey2, Base32Crockford.encode(eddsaPublicKeyBytes2))
+
+ val keyMaterial2 =
+ "FPQSHC6MWWJGEPXXVF76Q8C4PBJFE59RSGRGFQ3Z6ESR67HMG7FAK6JTCQ4ZKHSVNNCF53DX8FY55EA2193N2A6KD510AEV5TBVZRJR"
+ val keyMaterial1Bytes2 = crypto.keyExchangeEddsaEcdhe(
+ Base32Crockford.decode(eddsaPrivateKey2),
+ Base32Crockford.decode(ecdhePublicKey2)
+ )
+ assertEquals(keyMaterial2, Base32Crockford.encode(keyMaterial1Bytes2))
+
+ val keyMaterial2Bytes2 = crypto.keyExchangeEcdheEddsa(
+ Base32Crockford.decode(ecdhePrivateKey2),
+ Base32Crockford.decode(eddsaPublicKey2)
+ )
+ assertEquals(keyMaterial2, Base32Crockford.encode(keyMaterial2Bytes2))
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/KdfTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/KdfTest.kt
new file mode 100644
index 0000000..974f9aa
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/KdfTest.kt
@@ -0,0 +1,213 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.crypto.Kdf.HMAC_SHA256_BLOCK_SIZE
+import net.taler.wallet.kotlin.crypto.Kdf.HMAC_SHA512_BLOCK_SIZE
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class KdfTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+
+ @Test
+ fun testHmacSha256() {
+ // key smaller than block size
+ val key1 = "EDJP6WK5EG"
+ val message1 = "91JPRV3F41BPYWKCCGGG"
+ val expectedOut1 = "DYKV9QN2HVHMHQRGZ6XNJPPSGQZHA2JAVZB1676ACXYSNKQ0FQ30"
+ val out1 = Kdf.hmacSha256(Base32Crockford.decode(key1), Base32Crockford.decode(message1)) { crypto.sha256(it) }
+ assertTrue(Base32Crockford.decode(key1).size < HMAC_SHA256_BLOCK_SIZE)
+ assertEquals(expectedOut1, Base32Crockford.encode(out1))
+
+ // key bigger than block size
+ val key2 =
+ "AHM6JWS089GQ6S9K68G7CRBJD5GPWX10EXGQ6833E9JP2X35CGG64Y908HQQASVCC5SJ0GVJDXHPPSKFE9J20X3F41GQCVV9CGG66VVECSTQ6TBFDRQ0"
+ val message2 = "89P62RVB4166JXK5ECG4TRBMEHJQ488"
+ val expectedOut2 = "QGEK8853DW3GSQ4RHDYXZ0KX9R6E356R5495Z10KDNFNZ98V9FQG"
+ val out2 = Kdf.hmacSha256(Base32Crockford.decode(key2), Base32Crockford.decode(message2)) { crypto.sha256(it) }
+ assertTrue(Base32Crockford.decode(key2).size > HMAC_SHA256_BLOCK_SIZE)
+ assertEquals(expectedOut2, Base32Crockford.encode(out2))
+
+ // key exactly block size
+ val key3 =
+ "AHM6JWS0EDJP6WK5EGG6PSBS41MQ6831ECG6RVVECWG62WS0EHM6A832DHQP6TS0EDMQMS90DXK20J2D851J0MT884S3ADH07MG3CD0"
+ val message3 = "89P62RVB4166JXK5ECG4TRBMEHJQ488"
+ val expectedOut3 = "HAEWRWXEWWPRMT4W6E32GS0P6QMW85MMZZA5CX2BMJW36QZQFJ2G"
+ val out3 = Kdf.hmacSha256(Base32Crockford.decode(key3), Base32Crockford.decode(message3)) { crypto.sha256(it) }
+ assertEquals(HMAC_SHA256_BLOCK_SIZE, Base32Crockford.decode(key3).size)
+ assertEquals(expectedOut3, Base32Crockford.encode(out3))
+ }
+
+ @Test
+ fun testHmacSha512() {
+ // key smaller than block size
+ val key1 = "EDJP6WK5EG"
+ val message1 = "91JPRV3F41BPYWKCCGGG"
+ val expectedOut1 =
+ "ZMVHDTWE341YSE6YQ4S0Y6XESW7RVFG4WK2CE7K4ZESQ10125JJ2VTRBCHPAQTTKW9MT4H350ZXN8GMP2SBFG03SB6YK63QMHQRE2FG"
+ val out1 = Kdf.hmacSha512(Base32Crockford.decode(key1), Base32Crockford.decode(message1)) { crypto.sha512(it) }
+ assertTrue(Base32Crockford.decode(key1).size < HMAC_SHA512_BLOCK_SIZE)
+ assertEquals(expectedOut1, Base32Crockford.encode(out1))
+
+ // key bigger than block size
+ val key2 =
+ "AHM6JWS089GQ6S9K68G7CRBJD5GPWX10EXGQ6833E9JP2X35CGG64Y908HQQASVCC5SJ0GVJDXHPPSKFE9J2W829EGG6AY33DHTP8SBK414JR82C5GG4Y83MDWG62XKFD5J20RVFDSK7AWV9DXQ20XV9EHM20S39CXMQ8WS0C5Q6882N41T6Y83GE9JQCSBEEGG62RV3D5J6AVKMC5P20VV2EDHPAVK9EHWJW"
+ val message2 = "89P62RVB4166JXK5ECG4TRBMEHJQ488"
+ val expectedOut2 =
+ "0N9GF90FES6HXS344WB396CF78SQDRZ7CS9MRAVCVZPJ3PJ97HYSB5A3C7RCXTTYYSS4CV11X3DANKQD71YMTA46R3NS1X8A99YVAE0"
+ val out2 = Kdf.hmacSha512(Base32Crockford.decode(key2), Base32Crockford.decode(message2)) { crypto.sha512(it) }
+ assertTrue(Base32Crockford.decode(key2).size > HMAC_SHA512_BLOCK_SIZE)
+ assertEquals(expectedOut2, Base32Crockford.encode(out2))
+
+ // key exactly block size
+ val key3 =
+ "AHM6JWS0ESJQ4Y90EDJP6WK5EGG6PSBS41MQ6835F1GP6X3CF4G62WS0DHQPWSS0C5SJ0X38CMG64V3FCDNJ0WV9F9JJ0VV641T6GS90916M2GS0AD44281N64S20RBCCXQQ4TBMD1PJ0XV8D5HPG839ECG32CHR41H7JX35ECG62VK441GJ0V3FEGG6YSH0EHJQGX1E5RQ2W"
+ val message3 = "89P62RVB4166JXK5ECG4TRBMEHJQ488"
+ val expectedOut3 =
+ "A3ZAKHA0EADSBP832KBCPDNVB4GP876PMQEEB2F55ERTQFQ7KMYNTJ3SEWJEQRNH3EV2H0HSA740JE4JQH7GM8FC708KHF4A5FG9QNG"
+ val out3 = Kdf.hmacSha512(Base32Crockford.decode(key3), Base32Crockford.decode(message3)) { crypto.sha512(it) }
+ assertEquals(HMAC_SHA512_BLOCK_SIZE, Base32Crockford.decode(key3).size)
+ assertEquals(expectedOut3, Base32Crockford.encode(out3))
+ }
+
+ @Test
+ fun testRfc4231() {
+ val key1 = ByteArray(20) { 0x0b }
+ val data1 = "91MJ0N38CNS6A"
+ assertEquals(
+ "P0T4RRERVCW56Q58NZ7AY2ZH5E41VGG0S61KV9S6X4VPRBHJSZVG",
+ Base32Crockford.encode(Kdf.hmacSha256(key1, Base32Crockford.decode(data1)) { crypto.sha256(it) })
+ )
+ assertEquals(
+ "GYN7SQN5XXGSTKZGPGJ1M7BCP0HQKX72SS7C4Y3TT2SGAHF1FKFDNA1KPZBBH9R20E5JEKNEMFTE9FMXJ57EPRFHE0Q6JV107896GN0",
+ Base32Crockford.encode(Kdf.hmacSha512(key1, Base32Crockford.decode(data1)) { crypto.sha512(it) })
+ )
+
+ val key2 = Base32Crockford.decode("99JPCS8")
+ val data2 = "EXM62X10CHQJ0YB141VP2VKM41K6YWH0DSQQ8T39DSKKY"
+ assertEquals(
+ "BFEC2HNZC1TMWTG44GK0H5BNRXD00FR8KMKKK0WXXHCBJS7C711G",
+ Base32Crockford.encode(Kdf.hmacSha256(key2, Base32Crockford.decode(data2)) { crypto.sha256(it) })
+ )
+ assertEquals(
+ "2S5QMYZWZ0CY5RWNZFKKPNQ0ME3VTS125T1HZNGG4W6DFTH50NA9EP5ZEQ05N6AADM1MYSFRY3KFVJQAP6HMTJKB9DHPW1RA72YEEDR",
+ Base32Crockford.encode(Kdf.hmacSha512(key2, Base32Crockford.decode(data2)) { crypto.sha512(it) })
+ )
+
+ val key3 = ByteArray(20) { 0xaa.toByte() }
+ val data3 = "VQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEX"
+ assertEquals(
+ "EWZAJ7HPG074D1ADQ3NX14C1MWMNJ2CB7VWC28PSCDAH9KPNCQZ0",
+ Base32Crockford.encode(Kdf.hmacSha256(key3, Base32Crockford.decode(data3)) { crypto.sha256(it) })
+ )
+ assertEquals(
+ "Z9SV024XATH89VXGY1TPS28BX6RVBPYXHVM1MDJNZ0Z37CH7KMWVYFM4G9WTE8P80TT8B93YCZ40FEA6MCVVXT4M4ST2F22SW4S95YR",
+ Base32Crockford.encode(Kdf.hmacSha512(key3, Base32Crockford.decode(data3)) { crypto.sha512(it) })
+ )
+
+ val key4 = Base32Crockford.decode("041061050R3GG28A1C60T3GF208H44RM2MB1E60S")
+ val data4 = "SQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKED"
+ assertEquals(
+ "G9ARME4T8GY0X96CG6C9KWG87A2Z1YN3WNWFG1VT5RZZ8SS9CSDG",
+ Base32Crockford.encode(Kdf.hmacSha256(key4, Base32Crockford.decode(data4)) { crypto.sha256(it) })
+ )
+ assertEquals(
+ "P2X4CNHQ8P66K475N32ZC7AAYZJQDPBZZ55REBF7DY050DGYWFDTJ755R4DA4QNMTSWJEQ65F20679FHJX0H432F5QHAVTZB22H9HQ8",
+ Base32Crockford.encode(Kdf.hmacSha512(key4, Base32Crockford.decode(data4)) { crypto.sha512(it) })
+ )
+
+ val key5 = ByteArray(20) { 0x0c.toByte() }
+ val data5 = "AHJQ6X10AXMQ8T10AHS7AVK3C5T6JVVE"
+ assertEquals(
+ "MEV1CX3K207E0VGCF5P2JNAN5C",
+ Base32Crockford.encode(Kdf.hmacSha256(key5, Base32Crockford.decode(data5)) { crypto.sha256(it) }
+ .copyOfRange(0, 16))
+ )
+ assertEquals(
+ "85FTTRKHB05567A1F6Y8J7C7MR",
+ Base32Crockford.encode(Kdf.hmacSha512(key5, Base32Crockford.decode(data5)) { crypto.sha512(it) }
+ .copyOfRange(0, 16))
+ )
+
+ val key6 = ByteArray(131) { 0xaa.toByte() }
+ val data6 = "AHJQ6X10ANSPJVK741662WK7CNS20N38C5Q20GKCDXHPPBAKD5X6A82BCNWJ0B9091GQ6T109DJQJ826D5S76X0"
+ assertEquals(
+ "C3J32P8YW2V7Y3CA4TNCQXDQFY70QHH16WMCA5058R20Y3Q3FXA0",
+ Base32Crockford.encode(Kdf.hmacSha256(key6, Base32Crockford.decode(data6)) { crypto.sha256(it) })
+ )
+ assertEquals(
+ "G2S44RY7R6HYQDRMJF0XTYZ8PJDMDMFM3D5EXG8J3C0KF0ZRYD96PNPG6ZG5Y9CRQM7X48AXD8F555F69XSZCFRAXJ5S2PMRBNW6B60",
+ Base32Crockford.encode(Kdf.hmacSha512(key6, Base32Crockford.decode(data6)) { crypto.sha512(it) })
+ )
+
+ val key7 = ByteArray(131) { 0xaa.toByte() }
+ val data7 =
+ "AHM6JWS0D5SJ0R90EHJQ6X10ENSPJVK741GJ0V31E9KPAWH0EHM62VH0C9P6YRVB5NSPJYK541NPAY90C5Q6883141P62WK7CNS20X38C5Q20RKCDXHPPBBKD5X6A834C5T62BH0AHM6A83BCNWJ0VK5CNJ7683MDWG64S90D1GQ6T35CGG64SB6DXS6A832CNMPWSS0ENSPAS10C9WJ0X38CMG4GKA18CG62V37DXS6JX38DMQ0"
+ assertEquals(
+ "KC4ZZ9RVJGQWP9V3BYYDBC798JZXRRV49W3H74WAFX8N6Q1T6QH0",
+ Base32Crockford.encode(Kdf.hmacSha256(key7, Base32Crockford.decode(data7)) { crypto.sha256(it) })
+ )
+ assertEquals(
+ "WDXPMXTXS1YVN96ZN7WPWQHZZQFBTWFRGSS8K1JXYPHJT86DS52BC0HCNGY4K0NH1NFEPNE3WKF1A4T6EVXPVR24C1JWJX20ZA66MP0",
+ Base32Crockford.encode(Kdf.hmacSha512(key7, Base32Crockford.decode(data7)) { crypto.sha512(it) })
+ )
+ }
+
+ @Test
+ fun testKdf() {
+ val salt = "94KPT83PCNS7J83KC5P78Y8"
+ val ikm = "94KPT83MD1JJ0WV5CDS6AX10D5Q70XBM41NPAY90DNGQ8SBJD5GPR"
+ val ctx = "94KPT83141HPYVKMCNW78833D1TPWTSC41GPRWVF41NPWVVQDRG62WS04XMPWSKF4WG6JVH0EHM6A82J8S1G"
+ val expectedOut =
+ "GTMR4QT05Z9WF5HKVG0WK9RPXGHSMHJNW377G9GJXCA8B0FEKPF4D27RJMSJZYWSQNTBJ5EYVV7ZW18B48Z0JVJJ80RHB706Y96Q358"
+ val out =
+ crypto.kdf(64, Base32Crockford.decode(ikm), Base32Crockford.decode(salt), Base32Crockford.decode(ctx))
+ assertEquals(expectedOut, Base32Crockford.encode(out))
+
+ val salt2 = "94KPT83141V6AWKS41SP2V3MF4G76VK1CDNG"
+ val ikm2 =
+ "94KPT83MD1JJ0WV5CDS6AX10D5Q70XBM41NPAY90DNGQ8SBJD5GPR83MD1GQ8838C5SJ0RK5CNQ20RV8C5Q6ESB441K6YWH08X75A82MC5P6AWG"
+ val ctx2 =
+ "94KPT83141HPYVKMCNW78833D1TPWTSC41GPRWVF41NPWVVQDRG62WS04XMPWSKF4WG6JVH0EHM6A82J8S1J0WVF41VPGY90DSQQ8833C5P6R83DCMG6JVK6DWZG"
+ val expectedOut2 =
+ "C6TXT6GMVZ8JNWZF27SPD6939FK035Y3FTWXYT0W4EZX9FNJ5KH24MQSK9D0MW3G7T1BBZ4ERVQC1RE24BNDPJQ68ZRQ8S3XN4GNHC8"
+ val out2 =
+ crypto.kdf(64, Base32Crockford.decode(ikm2), Base32Crockford.decode(salt2), Base32Crockford.decode(ctx2))
+ assertEquals(expectedOut2, Base32Crockford.encode(out2))
+
+ val salt3 = "C4G76RBCEG"
+ val ikm3 = "ESJQ4Y90EDJP6WK5EGG6PSBS"
+ val ctx3 = "D5Q6CVR"
+ val expectedOut3 = "ZYMG67TD51XYS0ZM3QZKH8HF8FW7CTVT91NAWD0PGZM2QGTNYKMXAA4ZJBH5V633TJW9E124CRYEY"
+ val out3 =
+ crypto.kdf(48, Base32Crockford.decode(ikm3), Base32Crockford.decode(salt3), Base32Crockford.decode(ctx3))
+ assertEquals(expectedOut3, Base32Crockford.encode(out3))
+
+ val salt4 = "84G76XV5CNT20WV1DHT20EH9"
+ val ikm4 = "9NWJ0SBREHS6AVB5DHWJ0WV5CDS6AX10DDJQJ"
+ val ctx4 = "CDQPWX35F1T20TBECSQKY88"
+ val expectedOut4 = "YM46SN0HYJ0RNGY1V5S7WY1SD2RQ1Y17G5Z37JD46E2Z8KY11PH5EV772RQ5NQ9SBMGG"
+ val out4 =
+ crypto.kdf(42, Base32Crockford.decode(ikm4), Base32Crockford.decode(salt4), Base32Crockford.decode(ctx4))
+ assertEquals(expectedOut4, Base32Crockford.encode(out4))
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/RecoupTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/RecoupTest.kt
new file mode 100644
index 0000000..ea74e3c
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/RecoupTest.kt
@@ -0,0 +1,90 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.CoinRecord
+import net.taler.wallet.kotlin.CoinSourceType.REFRESH
+import net.taler.wallet.kotlin.CoinSourceType.WITHDRAW
+import net.taler.wallet.kotlin.CoinStatus.FRESH
+import net.taler.wallet.kotlin.crypto.Recoup.Request
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class RecoupTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+ private val recoup = Recoup(crypto)
+
+ private class RecoupRequestVector(val record: CoinRecord, val request: Request)
+
+ @Test
+ fun test() {
+ val vectors = listOf(
+ RecoupRequestVector(
+ CoinRecord(
+ coinSource = WITHDRAW,
+ coinPub = "9YW99NYH54FWG87TP3SKCGR9MRWYSVR75X42FN4YAJC9579CQBJ0",
+ coinPriv = "EPPYWTDVWM4CXW75J8AAGWW620C7DCC3B45TM31KHKPYMT9VM7DG",
+ denomPub = "020000X3T40FNGSM3Y1QFKX9H4JY5EP70Y2CKDHD29B5BEZCTWRMT6AC9SA0G5YJ1XVYY580K6S93SFCKM5PFKP96H3KXDNP58EVQPTYDB5S0QY4V8B873NYA7EYRH25NJ8MR2VP6F7WWVMBK3NR3FSFP17PHPGF279NBSRTXWZSJZFX6RCTR6VS5WMSYFHZCR0P8R6MGHDCB3QW4M3G2001",
+ denomPubHash = "DG3114X57XKHQ1XM6AN0P7D2B6J96SVFG09S3SF43ZXCYYK9PGX84XP3ZY7WY3QD9JE1BWS2T8DGR78QXZZAVGED79HES10HAPTWBX8",
+ denomSig = "AHE8DGMTTKNWGCVQYTV56CBWA81DH10BQEBAM0A5YGRAZXRPVHMZ5FH0XW1523QXSTXT3WMS1X7FDMEZ3BR898YEDTXDTHEMX6RS11KCPBAZCGTNPHKYF6RH9414Q0PYT5BZKGKWJNAFPWQS715NXEFZBY1D6RPTAN520REJ4RTREC9PP5D8WVQ3B66Q4ARYQ3CK49K0ZDME0",
+ currentAmount = Amount("TESTKUDOS", 0, 0),
+ exchangeBaseUrl = "example.org",
+ suspended = false,
+ blindingKey = "1Y29A3ABERGYJR8Y9HS7XS8AYYDAKV6BZSXMZ0WS5VDTS150C100",
+ status = FRESH
+ ),
+ Request(
+ denomPubHash = "DG3114X57XKHQ1XM6AN0P7D2B6J96SVFG09S3SF43ZXCYYK9PGX84XP3ZY7WY3QD9JE1BWS2T8DGR78QXZZAVGED79HES10HAPTWBX8",
+ denomSig = "AHE8DGMTTKNWGCVQYTV56CBWA81DH10BQEBAM0A5YGRAZXRPVHMZ5FH0XW1523QXSTXT3WMS1X7FDMEZ3BR898YEDTXDTHEMX6RS11KCPBAZCGTNPHKYF6RH9414Q0PYT5BZKGKWJNAFPWQS715NXEFZBY1D6RPTAN520REJ4RTREC9PP5D8WVQ3B66Q4ARYQ3CK49K0ZDME0",
+ coinPub = "9YW99NYH54FWG87TP3SKCGR9MRWYSVR75X42FN4YAJC9579CQBJ0",
+ coinBlindKeySecret = "1Y29A3ABERGYJR8Y9HS7XS8AYYDAKV6BZSXMZ0WS5VDTS150C100",
+ coinSig = "GBN5MVEY6JATGGSTX5YF32G3G204Y1PF9ASVXQFN895DWN5ZK3CBY2NHC8ATB1E9JWSV1QD4ECM0XHP8Y6DFZ1S02MYD5NBKZ45B018",
+ refreshed = false
+ )
+ ),
+ RecoupRequestVector(
+ CoinRecord(
+ coinSource = REFRESH,
+ coinPub = "2YE003173JB6WNQ9HS73Z468F11KDHWWVGCPDHDTD6AY5AVJPQPG",
+ coinPriv = "GCR4R26XTCFNS109ZYC0G6M374K1ACNES1YH2CESWD86JBAE22WG",
+ denomPub = "020000X9M8MQVNH28D4J4YFA5ZZNKGNR0423BXQZV00RRN754XTDQMS5YKWQ3KSN8NV4V7CHDM22CRJ4WWQW05FDZC7VN0KK4S8VK9PYPPXNW6FJKHBSEZ2X1FCJKRC3T6PK2BKQ422Y2ASE76ZZAH6RRQT4SQGZTV3TRTSBC5AECJ5Z6C4RX7XFBERKVB45DA7H3V53YCYX1C41ZY5G2001",
+ denomPubHash = "J0G3G880JJJD09923AAWNQQZHJVRQT71ZK8KZGYW7T1P18PCPZ72FBAKDW3EFZ3QFZEW72EYJ9K0FG3RFZTFADQKZDDN9YT6BT2PE70",
+ denomSig = "8HVKAGMKRQRWB1HX9WCPX3FJ0SVE24DCAWQSHX4ZMXZ1KFZDNF4F0Z4K6ZCW142B2WDEH0W848W8WKH8P6A6EJR7J635QEF78CSJFF0EX1FRS5VY484GEX0HH3BDRDFGTHXNQRTTF1DD5ETMEG1QNKA3SAB24XZXZNQ6RDGTK02MRETP859NGMDD2F94F58JH4HYGXMAY0X32",
+ currentAmount = Amount("TESTKUDOS", 0, 0),
+ exchangeBaseUrl = "example.org",
+ suspended = false,
+ blindingKey = "C5VPT5F925ADJWK48PR07KV2W66EZQN4KYE146NY77DFM8GFCTXG",
+ status = FRESH
+ ),
+ Request(
+ denomPubHash = "J0G3G880JJJD09923AAWNQQZHJVRQT71ZK8KZGYW7T1P18PCPZ72FBAKDW3EFZ3QFZEW72EYJ9K0FG3RFZTFADQKZDDN9YT6BT2PE70",
+ denomSig = "8HVKAGMKRQRWB1HX9WCPX3FJ0SVE24DCAWQSHX4ZMXZ1KFZDNF4F0Z4K6ZCW142B2WDEH0W848W8WKH8P6A6EJR7J635QEF78CSJFF0EX1FRS5VY484GEX0HH3BDRDFGTHXNQRTTF1DD5ETMEG1QNKA3SAB24XZXZNQ6RDGTK02MRETP859NGMDD2F94F58JH4HYGXMAY0X32",
+ coinPub = "2YE003173JB6WNQ9HS73Z468F11KDHWWVGCPDHDTD6AY5AVJPQPG",
+ coinBlindKeySecret = "C5VPT5F925ADJWK48PR07KV2W66EZQN4KYE146NY77DFM8GFCTXG",
+ coinSig = "HGPAWTM2ZXVBZWYVSPS6S9DMSQWSVEJCQ78BN6WG2VND3PA7BQNHVE6142CGYX0VA82G5YP9SAV5YDNPNQJH2FTY5M6VM92QF6CB228",
+ refreshed = true
+ )
+ )
+ )
+ for (v in vectors) {
+ assertEquals(v.request, recoup.createRequest(v.record))
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshPlanchetTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshPlanchetTest.kt
new file mode 100644
index 0000000..51eb5c6
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshPlanchetTest.kt
@@ -0,0 +1,112 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Base32Crockford
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class RefreshPlanchetTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+
+ private val vectors = listOf(
+ TestVector(
+ "A9M953G6CJWBD21S82784Q3ZFEZYYWWKB9M6K4WQTTJZ8VTHPTTSHN8W3RMJQCCEKAFM04QD1PAQEN80DR6K0MNKYP404KMYW494D6R",
+ 27,
+ FreshCoin(
+ Base32Crockford.decode("JR124F1W9Q984Y01Y323CN88MVCJN7D8BJHYHCST5NWJTNCJMCQG"),
+ Base32Crockford.decode("YQS09P0FGBWS2FYY4NE5EDB93MKAYATNT8GZ33GSM34KF9PW6YWG"),
+ Base32Crockford.decode("KMWCX1W2HDKQM55ZRDZ630JE75YNDP8S8X8J02F9VSVZMNBWQ70G")
+ )
+ ),
+ TestVector(
+ "MRQ6KTFKJ5JMQS8QV5Z189W110V7F6BJJQMDHVKY17HDMTY7MYSB1MKRJY0ZGF0SC1TSZQNT7NJPJVJE17VBSBH05MJ26SQVPXCM9R8",
+ 65,
+ FreshCoin(
+ Base32Crockford.decode("W6BVA4JQKDJ294HFBDQGZK0VGGKRRXG820WWPJ1Y2TJ9SQYEXZWG"),
+ Base32Crockford.decode("FKH9KW7F73V8F4KEYME899RTKED2DVRM3A74MJJ5YT3P3CCGFS60"),
+ Base32Crockford.decode("BNDT4TPMV2R7HJVJAA8ZPF9J3Q0V1ZHY1JQ76723CBBQ99RX0GR0")
+ )
+ ),
+ TestVector(
+ "SCHMBND9ZMHDHTWQTSF1M41W4WPF9GM3FR5PJHC5JTZM25XEEC7CWC5SEN8FEWM54MSH27B9DJG91AKZ2YZ7ZMERG14QTBYJC02K21G",
+ 89,
+ FreshCoin(
+ Base32Crockford.decode("J75FF26KDZPBQW960SBG81W24BAY0YA04ZJ6PKQ1V0NQ7K62XNC0"),
+ Base32Crockford.decode("QS7BMWNG7SAEX7Z4H6QF66J5Q8GAX7JWHD4JTM9A656N43RH66C0"),
+ Base32Crockford.decode("JTNY89C0Y69X0NGMK2KB2JGGQZXX41GASPBN1YRS7KKV0MTP84CG")
+ )
+ ),
+ TestVector(
+ "S8DC2D6N1TW1TJQFRVDM5Q2HZ9G66MWVS4651GB2ST49TPFK2KWCRSADH51YNCMNGWK4JFT2SKQNWVA17XAR7EC18Z0X214PM1RF2MG",
+ 37,
+ FreshCoin(
+ Base32Crockford.decode("QWP8560H613GSNR2A1NGVQ023V2BP8ZYBVJJ0E2Y66ZGSHCX92K0"),
+ Base32Crockford.decode("XF58PHSGFDC30CBWQ55KDFWBDSZJMQ3AA6DF4V9SQ4E2DEFBS7N0"),
+ Base32Crockford.decode("KQT2K5HDKVVBHD188XS0KFBM63B4MAWM42HABT9FH806GME1QXHG")
+ )
+ ),
+ TestVector(
+ "THX83Y6G26CYMF186V6GPP5A4DQ6624D9EGCA6R7DVW6T32P6E6Q0TDW0S9MJSRG84SR2AMK1KKS6D627EJ02XSNH0BXDNWD1W35BWR",
+ 31,
+ FreshCoin(
+ Base32Crockford.decode("QWCD3DMV7NX87SPHYFWE3STBZQ02Q1NMXJ8GQGAA6WZ4GF7B3C80"),
+ Base32Crockford.decode("7J26RGBJFRGAYX8XDZZZ8PE9PCW3DH5KFA7564WZ6F5ADC159KB0"),
+ Base32Crockford.decode("M0A1EPG9WFB02Y7R6YYE6V6Q8DRJ466XCD6N99YFDAGN1QVA62NG")
+ )
+ ),
+ TestVector(
+ "D949HYZA6MWNKBKGTRRP1SGFA4X7AZK7HG5XX9YBVMWA05B884WAYY7MRDQNQYMRGA7AE01V6EEWZDF9JXMQKSYG0G1FVSZG4096JV8",
+ 26,
+ FreshCoin(
+ Base32Crockford.decode("BXRTK601B5NRJV9BAE88SAVPSMA20ZWDH5P0T6WFY7BSM3SZD930"),
+ Base32Crockford.decode("5N4WJWFR1J1SDNJGC8G9AV3ET4MPF6TGA4S1PJNQRJ5BRDHEEJN0"),
+ Base32Crockford.decode("P7XFE4W0H444XBYPRHB9VBTHKAZHNJE4E0R7P4D5FT90W3JJRE20")
+ )
+ ),
+ TestVector(
+ "7CXEEDABFD2FM802314FE91J2DS7ZHCHDGX27SJHJWWJWJZ1NGZBNZJ8T55W1P9ZEBPVYCHDNKQWRG68K3EJD069723857R06XDYE00",
+ 2147483647,
+ FreshCoin(
+ Base32Crockford.decode("QCAK6CA31KJ7HX4V3QEFK9CENT3E6DVE76M4VY9ZQ030CM8TZQJG"),
+ Base32Crockford.decode("2V7HAFK2ATBB9MDWRR8P9PSPXBWMGFXW5G75WVGYCRZB0G5PDG9G"),
+ Base32Crockford.decode("6C02KA6496FG5X0SDSWTS4JJHH87GYNSW9FZE6S34AWG5RJGPWQ0")
+ )
+ ),
+ TestVector(
+ "9F65N3N1BZW7TT6EB4X2GCDC5BN3J2R33R7H0NWPZENN6B4Y41XYTVC9SA5NP0E4WQFQEJ63WNC5R0C8KBMKDA5JCXEKEGJYVMTR7VG",
+ 0,
+ FreshCoin(
+ Base32Crockford.decode("S6RM093DHGG882T39KWC48807CFC36SRGGX54DBB1Z6NDAMT875G"),
+ Base32Crockford.decode("2DY3KVKA978S85ERF6QYF73Q95KQ0YVE89KKQ8JYR0FX161PV1S0"),
+ Base32Crockford.decode("PCY47EC1APBKBG9HWQCTJN6FWNJPGGC6XDJTYWBHTJ9A952AGCK0")
+ )
+ )
+ )
+
+ @Test
+ fun testRefreshPlanchets() {
+ for (v in vectors) {
+ val freshCoin = crypto.setupRefreshPlanchet(Base32Crockford.decode(v.seed), v.coinNumber)
+ assertEquals(v.freshCoin, freshCoin)
+ }
+ }
+
+ private class TestVector(val seed: String, val coinNumber: Int, val freshCoin: FreshCoin)
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/Sha256Test.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/Sha256Test.kt
new file mode 100644
index 0000000..3209e05
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/Sha256Test.kt
@@ -0,0 +1,70 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Base32Crockford
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+@kotlin.ExperimentalStdlibApi
+class Sha256Test {
+
+ private val crypto = CryptoFactory.getCrypto()
+
+ @Test
+ fun testAbc() {
+ assertEquals(
+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ crypto.sha256("abc".encodeToByteArray()).toHexString()
+ )
+ }
+
+ @Test
+ fun testEmptyString() {
+ assertEquals(
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ crypto.sha256("".encodeToByteArray()).toHexString()
+ )
+ }
+
+ @Test
+ fun testAbc448bits() {
+ assertEquals(
+ "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1",
+ crypto.sha256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq".encodeToByteArray()).toHexString()
+ )
+ }
+
+ @Test
+ fun testAbc896bits() {
+ assertEquals(
+ "cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1",
+ crypto.sha256("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu".encodeToByteArray())
+ .toHexString()
+ )
+ }
+
+ @Test
+ fun testAMillionAs() {
+ val input = ByteArray(1_000_000) { 0x61 }
+ assertEquals(
+ "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0",
+ crypto.sha256(input).toHexString()
+ )
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/Sha512Test.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/Sha512Test.kt
new file mode 100644
index 0000000..24be282
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/Sha512Test.kt
@@ -0,0 +1,116 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Base32Crockford
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class Sha512Test {
+
+ private val crypto = CryptoFactory.getCrypto()
+
+ @Test
+ fun testAbc() {
+ assertEquals(
+ "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f",
+ crypto.sha512("abc".encodeToByteArray()).toHexString()
+ )
+ }
+
+ @Test
+ fun testEmptyString() {
+ assertEquals(
+ "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e",
+ crypto.sha512("".encodeToByteArray()).toHexString()
+ )
+ }
+
+ @Test
+ fun testAbc448bits() {
+ assertEquals(
+ "204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445",
+ crypto.sha512("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq".encodeToByteArray()).toHexString()
+ )
+ }
+
+ @Test
+ fun testAbc896bits() {
+ assertEquals(
+ "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909",
+ crypto.sha512("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu".encodeToByteArray())
+ .toHexString()
+ )
+ }
+
+ @Test
+ fun testAMillionAs() {
+ val input = ByteArray(1_000_000) { 0x61 }
+ assertEquals(
+ "e718483d0ce769644e2e42c7bc15b4638e1f98b13b2044285632a803afa973ebde0ff244877ea60a4cb0432ce577c31beb009c5c2c49aa2e4eadb217ad8cc09b",
+ crypto.sha512(input).toHexString()
+ )
+ }
+
+ @Test
+ fun testExchangeTvgHashCode() {
+ val input = "91JPRV3F5GG4EKJN41A62V35E8"
+ val output =
+ "CW96WR74JS8T53EC8GKSGD49QKH4ZNFTZXDAWMMV5GJ1E4BM6B8GPN5NVHDJ8ZVXNCW7Q4WBYCV61HCA3PZC2YJD850DT29RHHN7ESR"
+ assertEquals(output, Base32Crockford.encode(crypto.sha512(Base32Crockford.decode(input))))
+ }
+
+ @Test
+ fun testIncrementalHashing() {
+ val n = 1024
+ val d = Random.nextBytes(n)
+
+ val h1 = crypto.sha512(d)
+ val h2 = crypto.getHashSha512State().update(d).final()
+ assertEquals(Base32Crockford.encode(h1), Base32Crockford.encode(h2))
+
+ val s = crypto.getHashSha512State()
+ for (i in 0 until n) {
+ val b = ByteArray(1)
+ b[0] = d[i]
+ s.update(b)
+ }
+ val h3 = s.final()
+ assertEquals(Base32Crockford.encode(h1), Base32Crockford.encode(h3))
+ }
+
+ @Test
+ fun testIncrementalHashing2() {
+ val n = 10
+ val d = Random.nextBytes(n)
+
+ val h1 = crypto.sha512(d)
+ val h2 = crypto.getHashSha512State().update(d).final()
+ assertEquals(Base32Crockford.encode(h1), Base32Crockford.encode(h2))
+
+ val s = crypto.getHashSha512State()
+ for (i in 0 until n) {
+ val b = ByteArray(1)
+ b[0] = d[i]
+ s.update(b)
+ }
+ val h3 = s.final()
+ assertEquals(Base32Crockford.encode(h1), Base32Crockford.encode(h3))
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt
new file mode 100644
index 0000000..1306c14
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt
@@ -0,0 +1,493 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.crypto.Signature.PurposeBuilder
+import net.taler.wallet.kotlin.exchange.DenominationRecord
+import net.taler.wallet.kotlin.exchange.DenominationStatus.Unverified
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedBad
+import net.taler.wallet.kotlin.exchange.WireFee
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class SignatureTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+ private val signature = Signature(crypto)
+
+ private class PurposeBuilderVector(val purposeNum: Int, val chunks: List<String>, val result: String)
+
+ @Test
+ fun testSignaturePurposeBuilder() {
+ val vectors = listOf(
+ PurposeBuilderVector(42, listOf("46D6FNS4VAFW0"), "000004000002M8CTCZBJ9PMZR0"),
+ PurposeBuilderVector(
+ 1375883724,
+ listOf("7H2ENBHG1AVRD5QVKYCXKAZQQQGCSEAAZ4"),
+ "000007AJ098WRF24XAQ302NQGTBFQ7WSV6NZFFF0SJWMNY8"
+ ),
+ PurposeBuilderVector(251873401, listOf("KVSRVCV6J7QGYR0"), "0000048F0D47K7QKHPSPD4FF1XG0"),
+ PurposeBuilderVector(
+ 2126178285,
+ listOf("EJN927GRT1HNS0K53TBPWYB4", "QGSXW3NGHPJCVTBX3KHGW7GBC1SJWPK0", "XK7GA"),
+ "00000BKYQBKYTX5AJ4F1HM33BJ16A7MQDSWP9F1KVR7B13D4SQMQT7731RF0PR3K5SD61V6F0M"
+ ),
+ PurposeBuilderVector(
+ 871975602,
+ listOf("JC26GG7M", "D5GER58NSW3MX3AB2EA0", "HVB81PFV0YYQ2170H77TQPZTB21T314P9B1N3VSXEHZ88VEPHW"),
+ "00000E1KZ55B54R4D10F8TB0XGAHBKR79T6MP4WMHVB81PFV0YYQ2170H77TQPZTB21T314P9B1N3VSXEHZ88VEPHW"
+ ),
+ PurposeBuilderVector(
+ 366986877,
+ listOf("28", "48213TXMKY8H8EGRJ9XP00D9MHRHQG5N02W5GG0", "VR4CDW7WQ2FYQF3WBG60"),
+ "00000B8NVZ37T4H20G8YQD4ZJ4A3M64JFDG03AD4E4DW1D80Q1C41QG8RVRFSE4ZXEY7RQ0C"
+ ),
+ PurposeBuilderVector(
+ 1369171164,
+ listOf(
+ "SJTSKNX7W7CAP848GCESA09HQQQYQN50G1GY5AHPP12D4GMZJCXMCFPYECGEBF8",
+ "J70SVFRH8WN1DP7DZ60BZW0",
+ "275VB93D2MV6NN7V97QXC16VMX07JNCC5R3HBH1TSWTW5V72G1E66HP73ANWFXE6NG"
+ ),
+ "00000SJHKFJDSK5NK7BTFRERNCG8H0RXJM0K3FFFXFAA1031WAN3DC24T919Z4SV8RZDWWS0WPYS3GCXQW8MEAGPV3PZK05ZY08WQDD4DMAKCTPMZD4YZNG4VEKM0YANHGQ0E5E47B7KBGQCWA05RRT6RWDAQHZNRTP0"
+ ),
+ PurposeBuilderVector(
+ 2115762114,
+ listOf("Z7A3PQ7BV5G5D1P3F2EY9KN4GKVEG95KZD50", "SZ3PA4BSWF8T22TJMDZN5877Z9DQFC67K9E0"),
+ "00000D3Y3FVW5YEM7DEEQPB0AT3C6Y4XWK7A917PX0JB7YTASZ3PA4BSWF8T22TJMDZN5877Z9DQFC67K9E0"
+ ),
+ PurposeBuilderVector(
+ 1382722498,
+ listOf("0WEEY7GRA8Q8ZWS2NRP0", "FQ5YY3ZWJ64014A0SPWMCE4B58", "G03C6M9PZ45D99G4M2HZJ"),
+ "00000CAJDANW41RWXWF1GMHEHZSJ5BHCFQ5YY3ZWJ64014A0SPWMCE4B5A00DGTH6VWGNN560JGA7Y8"
+ ),
+ PurposeBuilderVector(
+ 1557782497,
+ listOf(
+ "CEB9E7Q87X12R172RZ8DAHSGGS60",
+ "M17JXQ290P209Q7EH759PW558FYZYTEHR40J18M81W",
+ "B918DJVWTERCHS54S0G93DFPVK9TV383DMRJR43ZTFZBAE3CVSFG"
+ ),
+ "00000MTWV7FY2RWPJWFEGFT25G2E5HYGTN3K11JCM17JXQ290P209Q7EH759PW558FYZYTEHR40J18M81XD451PBFK9V1J74MK4214DNYVED7BCD0DPK2B0GFZ9ZXD9RDKF5Y"
+ ),
+ PurposeBuilderVector(1662106597, listOf("621Y24CA3ZCHJJ8"), "000004B326XYAC43W48RM7YS354G"),
+ PurposeBuilderVector(
+ 393436430,
+ listOf(
+ "6WMDVK6XKGEAX152ZM4SHQ77DCRQB4RYJX0MGCZFVVK3XTG",
+ "8TKEY2F1PMAT396GC3E191182F34FXXR12MR7Z6WMG4S10GZX7RJG"
+ ),
+ "00000HGQEDEGWDS8VQ6DV70WNT2A5Z89K3EEETSHEP9HX5T190SYZQQ67VN4D9QF17GVA5D1MK861Q0MGGM17HJ7YYW0HAC3ZKEA82CGG8FYKW98"
+ ),
+ PurposeBuilderVector(
+ 1616746709,
+ listOf("ZQT1F901YWFDX6XZZB6R8PSQ1QJANCPF63T8RE09HRK2XQ7EE2K0F74GERH4GQ0"),
+ "00000BV0BPCDBZFM2YJ03XRYVTDVZYPDGHDKE3F4NASCYC7MHGW0K3H65VEEWW560YE90XH291E0"
+ ),
+ PurposeBuilderVector(
+ 691857007,
+ listOf("5JT5DM7KWVW7A7P764NTG0FJDWEQTQAGB253J547GXWX93204NM0"),
+ "00000A197KK6YB5MAV8F7SQREMFCEC9BN00Z4VRXFNEN0P4A74A8F1VSTJ6409B8"
+ ),
+ PurposeBuilderVector(
+ 582885064,
+ listOf("W5P2HP14MCNSKX4CZ4FZ0BDPSX2SV7MCE45V8PF7DW9EXAGK3TC0"),
+ "00000A12QRFCHRBC53C298SBK7T8SY8ZY0PVDKT5KPF8RW8BPHCYEVRJXTN167MR"
+ ),
+ PurposeBuilderVector(
+ 480383332,
+ listOf(
+ "49DZJY0K1WJFDK7P86Q1R2F150S50JQR8HQW059HBG9CR615N4V82",
+ "D0XXQKX9BB0HVJK34TKCZJ1E2JQKF06EQVXRRD2ZKX4QGXG",
+ "99JY6SS2HJ1J29GS1793CGE5BKSG"
+ ),
+ "00000NRWM88P88JVZ5W163S4YV6FCGDE3G4Y2A1JA15FGH3FR0AK2Q0JSGC2BA9PG5M3QPYFN5DC27EACCKADKY85RAAYDW0STZFQ31MBYFMJY3P99JY6SS2HJ1J29GS1793CGE5BKSG"
+ ),
+ PurposeBuilderVector(
+ 877078324,
+ listOf("RC", "K1PKX78BPGNQSSZCVRB3B6PJYR", "74"),
+ "000006HM8WKK9GWRDMZ9T2XM5DYEFV6Y2RTSNMQP74"
+ ),
+ PurposeBuilderVector(
+ 25488155,
+ listOf(
+ "FNG58X0W2A4HV4ZY5TKS73RGNHA8KZ3B4WWDJS2V1B27F3G6G08FNHM8NYBZ45Q8",
+ "NQGNDPDNW4G9AEHXB1BKXQXK84"
+ ),
+ "00000G01GKNHPZB0AHT1R4M93P9ZWBN7JE7H1B2MH7Y6P9SRV5J5P2P4EY70D00GZB38HBWQY8BEHBF1AVCVBR90JMX3TP2Q7VFV6G8"
+ ),
+ PurposeBuilderVector(
+ 749389285,
+ listOf(
+ "FVD8X18A3S2KHJ7143TCEHE3K083DRBG2CA0D766NMRVHX7MSN78NSYHZSWNH9V2DM",
+ "1TSPZ4VBKEBKCQEB2673H2FJHCZ6M19R2691NY79A0",
+ "P9VW044VY2TGKNNF4ZWZ1S2ACM4VJYCWP7ZGWRKR1264ZS2CQZGSXZ775AC41X5778"
+ ),
+ "00000X1CNB2YAZPTHT2GM7J5734E287MRX2W760G6VGQ04RM0TECDB9HQ3TF9KAEHBKX3ZKSB2KP4V8EPDQS6TWVJWV5VJRHHRW8KWMB7SN0AE0HJ8DFHTAGP9VW044VY2TGKNNF4ZWZ1S2ACM4VJYCWP7ZGWRKR1264ZS2CQZGSXZ775AC41X5778"
+ ),
+ PurposeBuilderVector(
+ 434249902,
+ listOf(
+ "VFERBNSH7X70",
+ "T5DWNBWMF3BPAGWSESKF7WGTTS3JHVKAAR3DR",
+ "SMCA0184DN3WE934BGFE4MCE14279NYCFF0PJDQ20JRHFZXSGXE6CP1BSMGR479HAC"
+ ),
+ "00000KRSW8GAXPYXGQBK2FTET5DWNBWMF3BPAGWSESKF7WGTTS3JHVKAAR3DSK8RM02G8VA7RWJ68Q0YW98RW284EKBWRYY1D4VE415H2ZZVK1TWCSC2QK91G8EK2MR"
+ ),
+ PurposeBuilderVector(
+ 1710327887,
+ listOf("S64XB965NEAQQ5287XHNT126BW"),
+ "00000635Y644ZJC9TPJCBAWNFEA4GFV3BM24CQR"
+ ),
+ PurposeBuilderVector(
+ 1247371119,
+ listOf("CSXX5W59E3S03412M29J0HG0R67BW4B3X946GS2T0TVYPH9HFP9FC0M7", "931KSGS0ZJG4A79BT55G0Q4Q"),
+ "00000EJAB5FPYSKVTBRAJW7J0682584K41301GCEQR8P7TJ8D1J5M1NQXD2K2ZCJYR18EJ637K1J1Z508MEJQMAB01E9E"
+ ),
+ PurposeBuilderVector(
+ 2024386767,
+ listOf(
+ "R2WZA0FKD57D418NB2KK84PJKHAVPEC6219X2KQJ78JFB76F0R",
+ "CNYD5TAC7Y3N58CN6VK9F7NBKD2NJJK3VJ91CZKBFZWDVVZB28",
+ "G7WQJVXXYT4JHA1NK22RXS45BM0W5H5JD7JNX3PWD2WZ27Y5Y1FB4"
+ ),
+ "00000SVRN6RCZG5SYM0Z6TAET82HAP576G9D572NQCWRC42KT57F4EH4YPECY1K5FK9EJK1ZGX9A359PWTBSXAWV8NCMMRYWJ8B7WTVZZ3EYZTRJG7WQJVXXYT4JHA1NK22RXS45BM0W5H5JD7JNX3PWD2WZ27Y5Y1FB4"
+ ),
+ PurposeBuilderVector(1182512118, listOf("KH32THSNNZGJPC0"), "000004A6FESZD7265N3KBBZ15CR0"),
+ PurposeBuilderVector(
+ 2029783771,
+ listOf("N7468QH02K49BTD16S1F6FB4NKNXB0Q410"),
+ "000007BRZG5DQAE8CHF20568JQMT2DJ2YCYP9B7BTP1E820"
+ ),
+ PurposeBuilderVector(
+ 1111140596,
+ listOf("6BN45PA2RCVRR8PJ6XKRJ9ZER2BR3MY9RQ290FADAG"),
+ "000008J27AMF8CQA8BCM5GSQHGHD4DV7H4KYXG4QG79WKHE4J0YMTN0"
+ )
+ )
+ for (v in vectors) {
+ val builder = PurposeBuilder(v.purposeNum)
+ for (chunk in v.chunks) builder.put(Base32Crockford.decode(chunk))
+ val encodedBytes = Base32Crockford.encode(builder.build())
+ assertEquals(v.result, encodedBytes)
+ }
+ }
+
+ private class PaymentSignatureVector(val publicKey: String, val hash: String, val signature: String)
+
+ @Test
+ fun testVerifyPaymentSignature() {
+ val vectors = listOf(
+ PaymentSignatureVector(
+ "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0",
+ "CW96WR74JS8T53EC8GKSGD49QKH4ZNFTZXDAWMMV5GJ1E4BM6B8GPN5NVHDJ8ZVXNCW7Q4WBYCV61HCA3PZC2YJD850DT29RHHN7ESR",
+ "JSNG99MX5W4AS7AEA8D4ADCHYTHFER0GX1N064E1XX48N513AXAEHDTG8ZT7ANWQK5HGCAXGEWN7TCBTYVG3RDPBTAS5HEP608KQ40R"
+ ),
+ PaymentSignatureVector(
+ "6WC3MPYM5XKPKRA2Z6SYB81CPFV3E7EC6S2GVE095X8XH63QTZCG",
+ "Z6H76JXPJFP3JBGSF54XBF0BVXDJ0CJBK4YT9GVR1AT916ZD57KP53YZN5G67A4YN95WGMZKQW7744483P5JDF06B6S7TMK195QGP20",
+ "T2Y4KJJPZ0F2DMNF5S81V042T20VHB5VRXQYX4RF8KRH6H2Z4JRBD05CCDJ6C625MHM5FQET00RDX2NF5QX63S9YDXEP0710VBYHY10"
+ ),
+ PaymentSignatureVector(
+ "2X3PSPT7D6TEM97R98C0DHZREFVAVA3XTH11D5A2Z2K7GBKQ7AEG",
+ "JSNG99MX5W4AS7AEA8D4ADCHYTHFER0GX1N064E1XX48N513AXAEHDTG8ZT7ANWQK5HGCAXGEWN7TCBTYVG3RDPBTAS5HEP608KQ40R",
+ "W0783H1KZ6GX58T5ZEH5VYMTXP7P7EA3KBKQ4Y8CN8M20GY8RNA4RX1AZG6TQ70NR4XG4EZ9D606P4RDAARD2SXTKA90BJMN9VEAC00"
+ )
+ )
+ for (v in vectors) {
+ // verification succeeds as expected
+ assertTrue(signature.verifyPayment(v.signature, v.hash, v.publicKey))
+ // verification fails which different signature
+ val size = Base32Crockford.calculateDecodedDataLength(v.signature.length)
+ val sig = Base32Crockford.encode(Random.nextBytes(size))
+ assertFalse(signature.verifyPayment(sig, v.hash, v.publicKey))
+ // verification fails which different hash
+ val hash = Base32Crockford.encode(Random.nextBytes(64))
+ assertFalse(signature.verifyPayment(v.signature, hash, v.publicKey))
+ // verification fails which different public key
+ val publicKey = Base32Crockford.encode(Random.nextBytes(32))
+ assertFalse(signature.verifyPayment(v.signature, v.hash, publicKey))
+ }
+ }
+
+ private class WireFeeSignatureVector(val wireFee: WireFee, val masterPub: String)
+
+ @Test
+ fun testVerifyWireFeeSignature() {
+ val type = "x-taler-bank"
+ val vectors = listOf(
+ WireFeeSignatureVector(
+ WireFee(
+ wireFee = Amount("TESTKUDOS", 0, 1000000),
+ closingFee = Amount("TESTKUDOS", 0, 1000000),
+ startStamp = Timestamp(1609470000000),
+ endStamp = Timestamp(1641006000000),
+ signature = "C77EZ56ZT4ACNPGP3PX881S42413N37NJVTRPNMM9BWRVGQBK2C157HHRF61RMYPAWW6QVWV5RVKEVBD3XAVJFXAEBM2HPNS5AMPY18"
+ ), "Y8JGNCPMM7XTF44FT7V1JRNNJQ6F4R7DZPXAKYYCJZJVEX2C45QG"
+ ),
+ WireFeeSignatureVector(
+ WireFee(
+ wireFee = Amount("TESTKUDOS", 0, 1000000),
+ closingFee = Amount("TESTKUDOS", 0, 1000000),
+ startStamp = Timestamp(1577847600000),
+ endStamp = Timestamp(1609470000000),
+ signature = "4F87Z12WQM7JXEKPRPGFZVGCZPNN6Q9RVP2NA0PZ57CYQP4QXV38EQW59X3K5WAQN82BX95X57775ZJGA5EB3VA5GV9S3JBA3RGX41G"
+ ), "0BJ4SX13W4Q64QDDSRBVTYSWT8C0X2HVB61QYSZM1B1W9JVCE0C0"
+ ),
+ WireFeeSignatureVector(
+ WireFee(
+ wireFee = Amount("TESTKUDOS", 0, 1000000),
+ closingFee = Amount("TESTKUDOS", 0, 1000000),
+ startStamp = Timestamp(1672542000000),
+ endStamp = Timestamp(1704078000000),
+ signature = "9YWES9YPR2W5ACCYE4ZE4S3PE62CVX01AXXSC9KT9PZ6B52D5GSBP4DG95Y024EDE5HFGP6FEZVPKQM8PA6J9WD2NXW3QBA34MEV61R"
+ ), "FWPD4E312RB8XZ22FGHCZGV2YPT7AX02XVRZEC7F53QKZJHY7H50"
+ )
+ )
+ for (v in vectors) {
+ // verification succeeds as expected
+ assertTrue(signature.verifyWireFee(type, v.wireFee, v.masterPub))
+ // different type fails verification
+ assertFalse(signature.verifyWireFee("foo", v.wireFee, v.masterPub))
+ // different WireFee wireFee fails verification
+ var wireFee = v.wireFee.copy(wireFee = Amount("TESTKUDOS", 0, 100))
+ assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // different WireFee closingFee fails verification
+ wireFee = v.wireFee.copy(closingFee = Amount("TESTKUDOS", 0, 100))
+ assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // different WireFee startStamp fails verification
+ wireFee = v.wireFee.copy(startStamp = Timestamp(v.wireFee.startStamp.ms + 1000))
+ assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // different WireFee endStamp fails verification
+ wireFee = v.wireFee.copy(endStamp = Timestamp(v.wireFee.endStamp.ms + 1000))
+ assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // different WireFee signature fails verification
+ val size = Base32Crockford.calculateDecodedDataLength(v.wireFee.signature.length)
+ wireFee = v.wireFee.copy(signature = Base32Crockford.encode(Random.nextBytes(size)))
+ assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // startStamp changes below one second don't affect verification
+ wireFee = v.wireFee.copy(startStamp = Timestamp(v.wireFee.startStamp.ms + 999))
+ assertTrue(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // startStamp changes below one second don't affect verification
+ wireFee = v.wireFee.copy(endStamp = Timestamp(v.wireFee.endStamp.ms + 999))
+ assertTrue(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // different masterPub fails verification
+ val masterPub = Base32Crockford.encode(Random.nextBytes(32))
+ assertFalse(signature.verifyWireFee(type, v.wireFee, masterPub))
+ }
+ }
+
+ private class DenominationRecordSignatureVector(val denominationRecord: DenominationRecord, val masterPub: String)
+
+ @Test
+ fun testVerifyDenominationRecordSignature() {
+ val vectors = listOf(
+ DenominationRecordSignatureVector(
+ DenominationRecord(
+ value = Amount("TESTKUDOS", 4, 0),
+ denomPub = "020000XX26WN5X7ECKERVGBWTFM3KJ7AT0N8T7RQB7W7G4Q5K0W1BT8QFQBMMTR925TC6RX4QGVVVXH2ZMJVWDRR58BRXNDCFBZTH7RNS1KVWZQGEWME1D6QX79R0V6V9S0NC9H8YP0W6MJD7YSV5VQZWCR1JXRTSS0HPHDV4V6AVX34TSMHGRDQXZYVTEBGVWNF8TNR2P5296TCZR0G2001",
+ denomPubHash = "MY54RXQ1WTZPFD3VZ4QJH6RHFPTYE78Y4DQ3GANTWK2Z0SQ99AK90K5H0P419EY4QWV6S4Q6QG52HYFCVRCPEQNG22RM3E7XW2YFJ9R",
+ feeWithdraw = Amount("TESTKUDOS", 0, 3000000),
+ feeDeposit = Amount("TESTKUDOS", 0, 3000000),
+ feeRefresh = Amount("TESTKUDOS", 0, 4000000),
+ feeRefund = Amount("TESTKUDOS", 0, 2000000),
+ stampStart = Timestamp(1593449948000),
+ stampExpireWithdraw = Timestamp(1594054748000),
+ stampExpireLegal = Timestamp(1688057948000),
+ stampExpireDeposit = Timestamp(1656521948000),
+ masterSig = "2PEF4EHTKE2R89KHQBZ6NAQW34YEMAP044DTJXJ9TFX224YW07BB2X1VHEYH425N2RNDZS9XFEAQK54GV6Q6CTNSE1Z038TZ4V3MM1R",
+ status = Unverified, isOffered = false, isRevoked = false, exchangeBaseUrl = "example.org"
+ ),
+ "C5K3YXPHSDVYPCB79D5AC4FYCKQ9D0DN3N3MBA5D54BG42SX0ZRG"
+ ),
+ DenominationRecordSignatureVector(
+ DenominationRecord(
+ value = Amount("TESTKUDOS", 0, 10000000),
+ denomPub = "020000XWWS2DFXM1G8YR6NXVF07R0DH7GRE1J7ZC2YENN7Q60ZHX9S7FEVQDBFP1041DN0GFASZR6A7RSJ3EYRV1YD5ACAZRDQNH5P5KEBTSK5XA76YYWTW5VA7N1V1VRVF0CF7VZ8GSJNQ91FSJGNKGR82DB7H82TPAGMR1B5DG5SY2WP3NB5YJD90H64E09C0YC8FCCWRGC09HFNPG2001",
+ denomPubHash = "J73HZJTR76GRZKBEKQC4X289056V5RMY4JS932XM8BAENQ83YAF9W2WYKRBN87TN8ENXP61JC7HMC7PYK9MZ8S289FCD07EM95AJGMR",
+ feeWithdraw = Amount("TESTKUDOS", 0, 1000000),
+ feeDeposit = Amount("TESTKUDOS", 0, 1000000),
+ feeRefresh = Amount("TESTKUDOS", 0, 3000000),
+ feeRefund = Amount("TESTKUDOS", 0, 1000000),
+ stampStart = Timestamp(1593450307000),
+ stampExpireWithdraw = Timestamp(1594055107000),
+ stampExpireLegal = Timestamp(1688058307000),
+ stampExpireDeposit = Timestamp(1656522307000),
+ masterSig = "HPBAY19C1B5H3FAYWKRQFS8VC693658SNSK3BB304BNAG950BS881GMVPVN0YBG67K9J9E4A9BFN7VAQEY1D5G6YAGVPFQWX0GRQ62G",
+ status = Unverified, isOffered = false, isRevoked = false, exchangeBaseUrl = "example.org"
+ ),
+ "NF8G14QFVWJSQPNFC3M2JKNMGJESS8ZWZX9V43PG40YXWRWA88VG"
+ )
+ )
+ for (v in vectors) {
+ // verification succeeds as expected
+ assertTrue(signature.verifyDenominationRecord(v.denominationRecord, v.masterPub))
+ // different masterPub fails verification
+ val masterPub = Base32Crockford.encode(Random.nextBytes(32))
+ assertFalse(signature.verifyDenominationRecord(v.denominationRecord, masterPub))
+ // different value fails verification
+ val value = v.denominationRecord.value + Amount.min(v.denominationRecord.value.currency)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(value = value),
+ v.masterPub
+ )
+ )
+ // different denomPubHash fails verification
+ val calculatedDenomPubHash = crypto.sha512(Base32Crockford.decode(v.denominationRecord.denomPub))
+ assertEquals(v.denominationRecord.denomPubHash, Base32Crockford.encode(calculatedDenomPubHash))
+ val denomPubHash = Base32Crockford.encode(Random.nextBytes(calculatedDenomPubHash.size))
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(denomPubHash = denomPubHash),
+ v.masterPub
+ )
+ )
+ // different feeWithdraw fails verification
+ val feeWithdraw = v.denominationRecord.feeWithdraw + Amount.min(v.denominationRecord.feeWithdraw.currency)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(feeWithdraw = feeWithdraw),
+ v.masterPub
+ )
+ )
+ // different feeDeposit fails verification
+ val feeDeposit = v.denominationRecord.feeDeposit + Amount.min(v.denominationRecord.feeDeposit.currency)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(feeDeposit = feeDeposit),
+ v.masterPub
+ )
+ )
+ // different feeRefresh fails verification
+ val feeRefresh = v.denominationRecord.feeRefresh + Amount.min(v.denominationRecord.feeRefresh.currency)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(feeRefresh = feeRefresh),
+ v.masterPub
+ )
+ )
+ // different feeRefund fails verification
+ val feeRefund = v.denominationRecord.feeRefund + Amount.min(v.denominationRecord.feeRefund.currency)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(feeRefund = feeRefund),
+ v.masterPub
+ )
+ )
+ // different stampStart fails verification
+ val stampStart = Timestamp(v.denominationRecord.stampStart.ms + 1000)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(stampStart = stampStart),
+ v.masterPub
+ )
+ )
+ // different stampExpireWithdraw fails verification
+ val stampExpireWithdraw = Timestamp(v.denominationRecord.stampExpireWithdraw.ms + 1000)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(stampExpireWithdraw = stampExpireWithdraw),
+ v.masterPub
+ )
+ )
+ // different stampExpireLegal fails verification
+ val stampExpireLegal = Timestamp(v.denominationRecord.stampExpireLegal.ms + 1000)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(stampExpireLegal = stampExpireLegal),
+ v.masterPub
+ )
+ )
+ // different stampExpireDeposit fails verification
+ val stampExpireDeposit = Timestamp(v.denominationRecord.stampExpireDeposit.ms + 1000)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(stampExpireDeposit = stampExpireDeposit),
+ v.masterPub
+ )
+ )
+ // different masterPub fails verification
+ val size = Base32Crockford.calculateDecodedDataLength(v.denominationRecord.masterSig.length)
+ val masterSig = Base32Crockford.encode(Random.nextBytes(size))
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(masterSig = masterSig),
+ v.masterPub
+ )
+ )
+ // different status does not affect verification
+ assertTrue(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(status = VerifiedBad),
+ v.masterPub
+ )
+ )
+ // different exchangeBaseUrl does not affect verification
+ assertTrue(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(exchangeBaseUrl = "foo.bar"),
+ v.masterPub
+ )
+ )
+ }
+ }
+
+ private class WireAccountSignatureVector(val paytoUri: String, val signature: String, val masterPub: String)
+
+ @Test
+ fun testVerifyWireAccount() {
+ val paytoUri = "payto://x-taler-bank/localhost/Exchange"
+ val vectors = listOf(
+ WireAccountSignatureVector(
+ paytoUri,
+ "7THNYN5G12GXZABHP187XJZ3ACTDKAWGHWYM2ERA1VGE4JMGPADXT37ZM8D4DVAQTSP8CR61VAD4ZZSWKRPP5KQ12JCTVHCKDD3KA3R",
+ "QPJSEA4SM8E67106C6MN2TMG4308J20C1T0D411WED1FJ00VF8ZG"
+ ),
+ WireAccountSignatureVector(
+ paytoUri,
+ "6JJDJ0HG3AGRTEY1FFGHR89367GPE4FS7TC8Z26N34PHFAMSRXQ4FA7P96CDS6625SRNAN4DY1NK4TNBQXKRXQ9QR82HEVCB2FF1E18",
+ "8QBE0SRR84GWJ1FX2QCGGNRZTWA2FCV6YQC98Q26DRRJ0QBQE930"
+ ),
+ WireAccountSignatureVector(
+ paytoUri,
+ "X5EYQJ388P8AH1PPYD2GFQ9Y1NGA7HV0TXAW6GJ7C0D6R6GAY0059HCDBE98TKJJT6MB1S660FV13DV46JDKJ622MR961XVGP6DG618",
+ "802M037TN8GHBXEGBPC7J41HJC06TWBWXYH36AYQHRJ7NVHJBQQ0"
+ )
+ )
+ for (v in vectors) {
+ // verification succeeds as expected
+ assertTrue(signature.verifyWireAccount(v.paytoUri, v.signature, v.masterPub))
+ // different paytoUri fails verification
+ assertFalse(signature.verifyWireAccount("foo", v.signature, v.masterPub))
+ // different paytoUri fails verification
+ val size = Base32Crockford.calculateDecodedDataLength(v.signature.length)
+ val sig = Base32Crockford.encode(Random.nextBytes(size))
+ assertFalse(signature.verifyWireAccount(v.paytoUri, sig, v.masterPub))
+ // different paytoUri fails verification
+ val masterPub = Base32Crockford.encode(Random.nextBytes(32))
+ assertFalse(signature.verifyWireAccount(v.paytoUri, sig, masterPub))
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/DenominationTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/DenominationTest.kt
new file mode 100644
index 0000000..f48c97d
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/DenominationTest.kt
@@ -0,0 +1,91 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.exchange
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.Timestamp.Companion.NEVER
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedBad
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedGood
+import net.taler.wallet.kotlin.exchange.Denominations.denomination1
+import net.taler.wallet.kotlin.exchange.Denominations.denomination10
+import net.taler.wallet.kotlin.exchange.Denominations.denomination2
+import net.taler.wallet.kotlin.exchange.Denominations.denomination5
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class DenominationTest {
+
+ @Test
+ fun testGetEarliestDepositExpiry() {
+ // empty selection info never expires
+ val infoEmpty = DenominationSelectionInfo(
+ totalCoinValue = Amount.zero("TESTKUDOS"),
+ totalWithdrawCost = Amount.zero("TESTKUDOS"),
+ selectedDenominations = emptyList()
+ )
+ assertEquals(Timestamp(NEVER), infoEmpty.getEarliestDepositExpiry())
+
+ // earliest expiry of single denomination is that of the denomination
+ val info1 = infoEmpty.copy(
+ selectedDenominations = listOf(SelectedDenomination(1, denomination10))
+ )
+ assertEquals(denomination10.stampExpireDeposit, info1.getEarliestDepositExpiry())
+
+ // denomination that expires earlier gets selected
+ val info2 = infoEmpty.copy(
+ selectedDenominations = listOf(
+ SelectedDenomination(3, denomination5.copy(stampExpireDeposit = Timestamp(42))),
+ SelectedDenomination(2, denomination2.copy(stampExpireDeposit = Timestamp(2))),
+ SelectedDenomination(1, denomination1.copy(stampExpireDeposit = Timestamp(1)))
+ )
+ )
+ assertEquals(Timestamp(1), info2.getEarliestDepositExpiry())
+
+ // denomination that expires at all is earlier than the one that never expires
+ val info3 = infoEmpty.copy(
+ selectedDenominations = listOf(
+ SelectedDenomination(2, denomination2.copy(stampExpireDeposit = Timestamp(NEVER))),
+ SelectedDenomination(1, denomination1.copy(stampExpireDeposit = Timestamp(1)))
+ )
+ )
+ assertEquals(Timestamp(1), info3.getEarliestDepositExpiry())
+ }
+
+ @Test
+ fun testIsWithdrawableDenomination() {
+ // denomination is withdrawable
+ assertTrue(denomination1.isWithdrawable())
+ // denomination is withdrawable when VerifiedGood
+ assertTrue(denomination1.copy(status = VerifiedGood).isWithdrawable())
+ // fails with VerifiedBad
+ assertFalse(denomination1.copy(status = VerifiedBad).isWithdrawable())
+ // fails when revoked
+ assertFalse(denomination1.copy(isRevoked = true).isWithdrawable())
+ // fails when not started
+ assertFalse(denomination1.copy(stampStart = Timestamp(Timestamp.now().ms + 9999)).isWithdrawable())
+ // fails when expired
+ assertFalse(denomination1.copy(stampExpireWithdraw = Timestamp.now()).isWithdrawable())
+ // fails when almost expired
+ assertFalse(denomination1.copy(stampExpireWithdraw = Timestamp(Timestamp.now().ms + 5000)).isWithdrawable())
+ // succeeds when not quite expired
+ assertTrue(denomination1.copy(stampExpireWithdraw = Timestamp(Timestamp.now().ms + 51000)).isWithdrawable())
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/Denominations.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/Denominations.kt
new file mode 100644
index 0000000..8cfd7fe
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/Denominations.kt
@@ -0,0 +1,119 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.exchange
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.exchange.DenominationStatus.Unverified
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedGood
+
+object Denominations {
+
+ private val validStart = Timestamp.now()
+ private val validExpireWithdraw =
+ Timestamp(Timestamp.now().ms + 1000L * 60L * 60L * 24L * 365L)
+ val denomination10 = DenominationRecord(
+ denomPub = "020000X0X3G1EBB22XJ4HD6R8545R294TMCMA13ZRW7R101KJENFGTNTZSPGA0XP898FJEVHY4SJTC0SM264K0Y7Q6E24S35JSFZXD6VAJDJX8FCERBTNFV5DZR8V4GV7DAD062CPZBEVGNDEJQCTHVFJP84QWVPYJFNZSS3EJEK3WKJVG5EM3X2JPM1C97AB26VSZXWNYNC2CNJN7KG2001",
+ denomPubHash = "7GB2YKDWKQ3DS2GA9XCVXVPMPJQA9M7Q0DFDHCX5M71J4E2PEHAJK3QF3KTJTWJA33KG0BX6XX0TTRMMZ8CEBM4GSE2N5FSV7GYRGH0",
+ exchangeBaseUrl = "https://example.org",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ isOffered = true,
+ isRevoked = false,
+ masterSig = "N9ADAGY43VTDA000NMW9NFKY8HBTTNPEWP9W1A1ATCAFVA1A9F1MD7HAJ6M4QQ3SVYJXCV1S9Z1ZMKXP9YKD3PFGNK0VQD1ZZ502420",
+ stampExpireDeposit = validExpireWithdraw,
+ stampExpireLegal = validExpireWithdraw,
+ stampExpireWithdraw = validExpireWithdraw,
+ stampStart = validStart,
+ status = Unverified,
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 10)
+ )
+ val denomination8 = denomination10.copy(
+ denomPub = "020000X61XTJPQHC9Z3GEF8BD0YTPGH819V6YQDW7MZ74PH9GC2WZGH1YPBHB6F6AJG7C91G19KTSSP9GA64SFZ26T06QK6XEQNREEFV6EMVRCQRYXV3YVA5ANJQRFJVMQG3DQG6Q0ANQWRBZGZSX43MNJQ0NGZY2X1VHJ0351RC83RMPYV1JFSZ2J1JZ2MN5AJF6QMCBBJN0V5TF3EG2001",
+ denomPubHash = "M496WWEBC8KN4MYB73V78F4DCXXMFWR2X33GWDA92X98MC04E120H9NVNKN70J3NP7ZZ91BE7CX65NYHQEG5EQK5Y78E7ZE3YV8E868",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 4000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 5000000, value = 0),
+ masterSig = "GNWDZNQHFECF2SPVWZ5D0KV7YEGJ0J86ND815B512ZSBNKSCSN7PCKE5GJTXV0WZNS3AYTDJYER3W1HXSST4TMBMAR3FY3ETRNRS20G",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 8)
+ )
+ val denomination5 = denomination10.copy(
+ denomPub = "020000XFF9HD3GJXVA9ARQD76BW2G9K65F6CVDPWSRYVE5HY7EVVBK1PK1XX2HE2P3BA3A9MJT9ESY1XNKK7TTF8DRE33C22EHPNNBPPQC1D1MHEE3YJHNF8PG0W6DTE406R6VHCZ0VHEE5HNTEPWMAHJ5J0VVY1ESGAXWE1SGSY82FCQARWV45MNGYZMBN2M55CG3SQXJ8STRPHEM1G2001",
+ denomPubHash = "4HKTYHDDCJXJW7Z215E3TFQWBRYKZA4VS41CZKBE8HH1T97FV4X9WSM62Q7WEZTZX3Y60T4Y8M4R0YYA3PVSEND1MZCJBTD4QZDMCJR",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ masterSig = "0TBJSTZTWMKBT71ET1B7RVFB1VT84E10G3SENPSMPRYTFBH07WSDS5VA9PW5T0EYEABDKGAYDRQ0XA62GYQXZ4SBV2CTHHFY0TFNC18",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 5)
+ )
+ val denomination4 = denomination10.copy(
+ denomPub = "020000Y3JVXB3XTC7JTK3CYEMSXHEX8956DN9B4Z6WB3J1H8D5Y3BVTY8EE5BC5JSRJKM0VAK6BXSHVRGQ6N43BF132DPNSJDG4CD8JA4A856HVCNFSNP0DY21TJYN8GJ36R1T0VKTVH2SRMT4QN1QQZC0VQ5ZV2EJJMCSVYVKV8MZC9NG5K9PGNQKBV64E34H1K9RSFEJE7306ZH07G2001",
+ denomPubHash = "XRKJ5750TW2ZNQYGQ87TESYF2X942RBCG7EKXB8QKMFGB433EK3567SDSE9EFNBYTEH3PHPTK22V8XNSJ090DHYX8EW9BE1Q8JCZW7G",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 4000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ masterSig = "VFKWHPFNEKD36A0A0TWVTEN98P4TCVBTRMJFJF1E1Q2578J9DXAY53Y7Q2D3BX01WKPM7QBCVF9WBMB4GTXVFP08QPZP086RV9PAP2G",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 4)
+ )
+ val denomination2 = denomination10.copy(
+ denomPub = "020000XB8Y8WVSBM0WBY6NMS0MTNN71PEA6KGKTV5FF27RRR8V1V0GPWYAFDMD2XT6E265QPNH079KM25ZZ97NGFRDVHW0Y7JPWA9C8MJY8DB7APYBRMD9XYA0N1569VFW2NPP4FGQJ865RVE94F35NSG0M4W80CG6WXXWW1ERRM7F2AGRZE9FS049183ANEDFB7QN4H62GDWS7039GG2001",
+ denomPubHash = "CTH34KGWJKQ3C264DQJCRPX97G5SWGWQQ8EMBXVH4DKGTPJ78ZA5CWZB9SDT538Z6S118VQ3RNX3CVC1YXEFN0PXR0D896MDNCGZW3G",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 4000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ masterSig = "BTZYYEYBZK1EJJ47MQHEY7Q8ARBK4PT0N92TYEEPEZ54P0NTN6FT50AGKCCQCWQ8J74D8MTZFAX3PRDWSH30JPVREAQKGVGEVYXN00G",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 2)
+ )
+ val denomination1 = denomination10.copy(
+ denomPub = "020000XBEVGYETENRYN30AMTT006NPFG5QNHHJXESR9YRZ0J1Y0V00ASMSK7SGKBZJW1GKW05AGNXZDGHAT8RP5M87H2QZTCZHNWW0W4SFWSHWEMRK57DQ3Z7EYQ3XMFX8E2QNZNT9TB4J0MMDP833Y0K7RCQ3X5A584ZBQ9M0T03KTF1QTS2Z7J0BRCEVMK7CCM5WYCAVDPP44Z23HG2001",
+ denomPubHash = "KHRCTNBZHY653V3WZBNMTGGM3MSS471EZ4F6X32HJMN17A47WBBM5WHCRNK8F27KB6Q45YMEN832BKYNKBK1GCRXKDP0XTYC3CYTRWR",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ masterSig = "X029BKSB9PRKE831VDV1X3ND441E0NNQCDJ2S9NKPG7F17R0Y6QCGKWCSHCHE96NW5XS13RK880H90ZSFY7HNJ17F4NSR1EM5FR2M10",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 1)
+ )
+ val denomination0d1 = denomination10.copy(
+ denomPub = "020000YVVE91KXE9S8JV5P4PWZJWBJD39Y6S00FA3CV6RP77A8PTKJWNE3W0W8ZHDR3CSKA17FT56PMC97RWV3RNG753B1WQYXEWNJA76GD5T2PA33BN08CQ07KP4M2K9R6A3N9VD6W8D3DK55W18Y7TBKAHCJBQ3AZ50VHSF1ZPM2XVJ238SKK1379WNHMK4VDJQ35H20QSF3GPWPKG2001",
+ denomPubHash = "C26V15X3BESS1CZCSP4BNSRJ8BK8DSGFHNB0WG1GZ6ZF6FR37WGSVEQ85A61X6Z103P8MY7XGQZ60VAX78V3E5GERWJTJAP0Q5QB7A8",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ masterSig = "JSJXWJXN4532C07CBH4DZZ6YC5S3TPH3H8FG8RMGX0Z647GX2NYEK02NRN3C4GDS9Q0ZT6QE7T8EGYGEF9RZ9FCV65TZWC1ZP83DT1R",
+ value = Amount(currency = "TESTKUDOS", fraction = 10000000, value = 0)
+ )
+ val denomination0d01 = denomination10.copy(
+ denomPub = "020000X67SMNYMCR1HFZW4KEATXGXRA983JE5VW1JE4108XB7Z40BTJC1MV59Y9K4Y35E3MPPF73BJQA3KVT0FBT89R6ZYNZ77TSC5DMFV5E55DT4JB4S9K4C2V7GM8Z8QZ0KMCH0YK4PAX1WSKCEQFNRVKD3VH9WTVQ0CNV7Z1JVRHBKCSZNX62TRQ6JRZ05JEANT3C41SQ6MKSQZKG2001",
+ denomPubHash = "VS0E7ABRZ9NZMTDAHVKQ4QH1S1Q2PYWXFXKF0P84VSHCDBM4S6QK1G1D495TABN4AXVX049P7JESNRRQVHW37BNER4XKZSQT3XA61DG",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ masterSig = "CF3638K8QBG91D66JZRRGWV8J53A0ZGBYJDMKYK293DNAA2ASM1M346C0YG9V7HWH8E2A7FPVR0HH2QE7DHD9GW0EN19VSER4S5F23R",
+ status = VerifiedGood,
+ value = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0)
+ )
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/KeysTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/KeysTest.kt
new file mode 100644
index 0000000..a6b0c98
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/KeysTest.kt
@@ -0,0 +1,316 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.exchange
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.getMockHttpClient
+import net.taler.wallet.kotlin.giveJsonResponse
+import net.taler.wallet.kotlin.runCoroutine
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class KeysTest {
+
+ private val httpClient = getMockHttpClient()
+
+ @Test
+ fun testFetchKeys() {
+ val expectedKeys = Keys(
+ denoms = listOf(
+ Denomination(
+ value = Amount.fromJSONString("TESTKUDOS:5"),
+ denom_pub = "040000Z9TH9RPTA1BXF6Z89HM7JGXTPD5G8NNBWQWF7RWQGNAATN84QBWME1TGSWZ79WPQ62S2W2VHG2XBH66JDJ0KM8Q2FQ3FGBZQGNJVFNA9F66E6S3P36KTMWMKWDXWM9EX1YHSHQ841AHRR8JVDY96CZ13AJF6JW95K59AE8CSTH5ZS9NVS0102X92GK8JW2QX2S4EE25QNHK6XMXH3944QMXPFS7SFCMV623BM62VNPVX8JM424YXPJ09TXZAH2CF3QM5HDVRSTDRDGVBF6KZVRFM852TMVMYPVGFA9YQF6HWNJ8H5VCQ3Z9WWNMQ3T76X4F1P6W2J266K8B3W9HKW2WJNK3XHRAVC4GCF07TC0ZNAT0EDAAKV429YAXWSK952BPTY98GVP5XZQG2SE0Q5CF3PV04002",
+ fee_withdraw = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_deposit = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refresh = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refund = Amount.fromJSONString("TESTKUDOS:0.01"),
+ stamp_start = Timestamp(1582484881000),
+ stamp_expire_withdraw = Timestamp(1677092881000),
+ stamp_expire_legal = Timestamp(1897844881000),
+ stamp_expire_deposit = Timestamp(1740164881000),
+ master_sig = "2SDD44VVBD52XEV0A9R878BC60J51VKK0H5ZS6CPJ7Z738A8V4KPXCF70KFZAY2567400C2GEWNNVXF6PYD7HKX3D2M63WCNPJSE010"
+ ),
+ Denomination(
+ value = Amount.fromJSONString("TESTKUDOS:2"),
+ denom_pub = "040000XV91V0M7H906Y7R371YX2XAK1V5B2TRFD8ZM9WYJ495TP08NCVEDNFXS2KZBJR4808VZ52PNNQSYVQ2T3J7867MZQY1QZ9N8YQWQWCKSYAY8A07E5SYAK0G0KRTCN5VZ7JXE2YCNT7Q3RT9TGAZBSK5V1ZRRK6HX4C1YFKPWWP4TBVJ8DJMS43WKR4CR4S9T02YXVGR6GSDMR7GHBD89JHCEQ8V2K58Y5XVDGRRQYNBG9Q5XWDMV7GKN24JCPCEKSZZP5XYPXYJX2Z2JZ179M9FQV0PEWFJ4DP7AP14XE54FH97YP9398KA31ECVDY7PHMKMZ6E79Z9FSCXH3WSCXMHBWFGWPRG5ZG1P6HR71VKH4F0Y998JNE4G40JH2VSXP3035AR7HAMJGB56CYHH60EWD904002",
+ fee_withdraw = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_deposit = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refresh = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refund = Amount.fromJSONString("TESTKUDOS:0.01"),
+ stamp_start = Timestamp(1582484881000),
+ stamp_expire_withdraw = Timestamp(1677092881000),
+ stamp_expire_legal = Timestamp(1897844881000),
+ stamp_expire_deposit = Timestamp(1740164881000),
+ master_sig = "WX1RDTW1W1K7FPFQQ9MCJ8V5CJP8ZX2SGHQG2YS34BY8AMSR3YQ92HE2HT1JMP4W06J63RZ0BR2MKDBX54GV6QKD4R00N9HXN2S283R"
+ ),
+ Denomination(
+ value = Amount.fromJSONString("TESTKUDOS:1"),
+ denom_pub = "040000XCBJE9TDDZATYSDR51D0DKMY5NW8FMJ8YQ1Y4F40SPPTKFMD3FWH38NSQZ1YB621TCFH5RBN5J3SFR5SG4789G27FA90E605GG9AXYTXXPJ9HYPAVAVS6V4XCSC17HKX2M2NSX5D0PPETDGKQD04G498VS36YY4WTB5SYG4SV9MKPVZ5WG2WNP3MA77TFZSHK5HBHZBEW0S1TRKGSCDNBRHYB240M84YM1Y7EJ7BXKJK4GRR1GS16DJ2RA1YEQ1AAXH0GP6RRAEJE8D2JFSH05P3KR1GB97NMX6VD8DCAX45416F888EYQR4M6R820FJVZ6FYV9CCMZ3M10B64N6G4QFNKFNAV2ENPVVG4A3R0AAA6STJ7E5Z05GEKW35SHM14HY9CEGM7D1ZEKHZJYA9P6WH504002",
+ fee_withdraw = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_deposit = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refresh = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refund = Amount.fromJSONString("TESTKUDOS:0.01"),
+ stamp_start = Timestamp(1582484881000),
+ stamp_expire_withdraw = Timestamp(1677092881000),
+ stamp_expire_legal = Timestamp(1897844881000),
+ stamp_expire_deposit = Timestamp(1740164881000),
+ master_sig = "65G9FWQPA4YKJEM7D37079D4MY81D47KD1280RG7BRH85XZQ2N13FJPV9N8AEASK9CTGNX1HKX0GTRBJ5C49H4YRY0E4CYVPNH06W18"
+ ),
+ Denomination(
+ value = Amount.fromJSONString("TESTKUDOS:10"),
+ denom_pub = "040000XZDZK4BPPPXR7MYKK2AF4WF95EH3VF8WEX7WDX4HEWXSB5XX10N4V5RHFSK0TSBKNC9CRNVGK3WJ42S3Z9SB4Q3M4DQQ7DKCGKED6WBKENHT8JX51K1VR5JKCMAFBNM6DR5MNRGKFC2MDRQ0Y4BCXHKEMRD65C6JPBKYW9HJH66FGT22WMBV0AV7P60CKR13MQG6FKWW3TZW3XXHVY2VX9MJN6VQFPS6NQGGTNXZV2SK2X5MJAJME7RN9BNZ5ZBTW1CYMVCHBSVGBFPRC68W78PW44VP402VD12KG2AWKPD4DRBAA85HM1DN1KADYQ498QHYGEB3T3HH990HRV8PSNBGYCHB87JTVYMJ4N2PSP2FCX0H6FRTW1FQY05EB7D8BFXM95DNRCHVQSHBZ9RP7NZFA304002",
+ fee_withdraw = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_deposit = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refresh = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refund = Amount.fromJSONString("TESTKUDOS:0.01"),
+ stamp_start = Timestamp(1582484881000),
+ stamp_expire_withdraw = Timestamp(1677092881000),
+ stamp_expire_legal = Timestamp(1897844881000),
+ stamp_expire_deposit = Timestamp(1740164881000),
+ master_sig = "M423J7CJPACTPBYCFVR87B44JAJKAB2ME8C263WGHJSA8V8444SX428MVC9NF4GD08CKS9HY0WB4B8SEZ3HJFWKXNSH80RBJXQC822G"
+ ),
+ Denomination(
+ value = Amount.fromJSONString("TESTKUDOS:0.1"),
+ denom_pub = "040000YKYFF6GX979JS10MEZ16BQ7TT6XBTE0TBX6VJ9KSG7K4D91SWJVDETNKQJXAFK9SAB3S31FZFA0Y0X22TKRXKCT7Z4GZCCRJJ12T1A5M4DWRTZDFRD3FE495NXHVPFM96KXMKH1HABTDDFZN0NWQ3NBJ6GNXD40NJ95E955X948JHBDJZWM3TEAK4XFJX8056XFDHVNXSF4VN14RR1WD1J5K7JPS61SKRNF3HT6NZA823PZW2KPV2KVBMMP615A922ZNJGVQDTW5TYWTK5DCBGG1YEKQRYF39NX9X722FZK98BTMHHH6WZFCKBT096G9BKSHSJW3VE8KKPCN8XGWYYPD3158HRKSA28BJQ9XJVVB6FDCGZ154WWGGSGW82BDYDH7ZHJBMS046AG0ND4ZCVR2JQ04002",
+ fee_withdraw = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_deposit = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refresh = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refund = Amount.fromJSONString("TESTKUDOS:0.01"),
+ stamp_start = Timestamp(1582484881000),
+ stamp_expire_withdraw = Timestamp(1677092881000),
+ stamp_expire_legal = Timestamp(1897844881000),
+ stamp_expire_deposit = Timestamp(1740164881000),
+ master_sig = "RKZKGR0KYKY0VZ26339DZKV8EZJ2HRRQMFSAJDHBG3YHEQNZFHKM8WPYCH9WHXTWBB10GQN9QJKFDJJF2H6D5FT801GF87G153PTJ18"
+ ),
+ Denomination(
+ value = Amount.fromJSONString("TESTKUDOS:1000"),
+ denom_pub = "040000Y9PBY1HPBDD4KSK9PBA86TRY13JQ4284T4E0G4SEREQ8YM88PZHKW1ACKT1RTWVTBXX83G54NFVYRJQX9PTDXDJ1CXSS42G8NYMW97NA6NNNASV69W1JX39NTS1NVKXPW4WMBASATSNBTXHRT92FFN2NAJFGK876BNN3TPTH57C76ADAQV43VFF7CYAWWNYZAYGQQ1XY1NK34FJD778VFGYCZ1G9J8XPNB92ZKJBZEZKSNBRNH27GM5A736AFSGP7B4JSCGD0F4FMD1PDVB26MM9ZK8C1TDKXQ5DJ09AQQ55P7Q3A133ASPGBH6SCJTJYH8C9A451B0SP4GDX2ZFRSX5FP93PY4VKEB36KCAQ5E2MRZNWFB6T0JK0W7Z7NXP5FW2VQ4PNV7B2NQ3WFMCVRSDSV04002",
+ fee_withdraw = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_deposit = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refresh = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refund = Amount.fromJSONString("TESTKUDOS:0.01"),
+ stamp_start = Timestamp(1582484881000),
+ stamp_expire_withdraw = Timestamp(1677092881000),
+ stamp_expire_legal = Timestamp(1897844881000),
+ stamp_expire_deposit = Timestamp(1740164881000),
+ master_sig = "FJPQKECRKVQSTB9Y983KDGD65Z1JHQKNSCC6YPMBN3Z4VW0AGC5MQM9BPB0YYD1SCMETPD6QB4X80HWE0ZDGWNZB1KND5TP567T4G3G"
+ )
+ ),
+ master_public_key = "DY95EXAHQ2BKM2WK9YHZHYG1R7PPMMJPY14FNGP662DAKE35AKQG",
+ auditors = emptyList(),
+ list_issue_date = Timestamp(1592161681000),
+ recoup = emptyList(),
+ signkeys = listOf(
+ SigningKey(
+ stamp_start = Timestamp(1592161681000),
+ stamp_expire = Timestamp(1594580881000),
+ stamp_end = Timestamp(1655233681000),
+ key = "0FMRBH8FZYYMSQ2RHTYYGK2BV33JVSW6MTYCV7Y833GVNXFDYK10",
+ master_sig = "368HV41Z4FNDXQ7EP6TNAMBSKP44PJAZW27EPH7XJNVG2A6HZQM7ZPMCB6B30HG50S95YD1K2BAJVPEYMGF2DR7EEY0NFBQZZ1B8P1G"
+ ),
+ SigningKey(
+ stamp_start = Timestamp(1594580881000),
+ stamp_expire = Timestamp(1597000081000),
+ stamp_end = Timestamp(1657652881000),
+ key = "XMNYM62DQW0XDQACCYDMFTM5GY7SZST60NH7XS9GY18H8Q9N7QN0",
+ master_sig = "4HRJN36VVJ87ZC2HZXP7QDSZN30YQE8FCNWZS3RCA1HGNY9Q0JPMVJZ79RDHKS4GYXV29PM27DGCN0VB0BCZFF2FC6FMF3A6ZNKC238"
+ )
+ ),
+ version = "7:0:0"
+ )
+
+ httpClient.giveJsonResponse("https://exchange.test.taler.net/keys") {
+ """{
+ "version": "7:0:0",
+ "master_public_key": "DY95EXAHQ2BKM2WK9YHZHYG1R7PPMMJPY14FNGP662DAKE35AKQG",
+ "reserve_closing_delay": {
+ "d_ms": 2419200000
+ },
+ "signkeys": [
+ {
+ "stamp_start": {
+ "t_ms": 1592161681000
+ },
+ "stamp_expire": {
+ "t_ms": 1594580881000
+ },
+ "stamp_end": {
+ "t_ms": 1655233681000
+ },
+ "master_sig": "368HV41Z4FNDXQ7EP6TNAMBSKP44PJAZW27EPH7XJNVG2A6HZQM7ZPMCB6B30HG50S95YD1K2BAJVPEYMGF2DR7EEY0NFBQZZ1B8P1G",
+ "key": "0FMRBH8FZYYMSQ2RHTYYGK2BV33JVSW6MTYCV7Y833GVNXFDYK10"
+ },
+ {
+ "stamp_start": {
+ "t_ms": 1594580881000
+ },
+ "stamp_expire": {
+ "t_ms": 1597000081000
+ },
+ "stamp_end": {
+ "t_ms": 1657652881000
+ },
+ "master_sig": "4HRJN36VVJ87ZC2HZXP7QDSZN30YQE8FCNWZS3RCA1HGNY9Q0JPMVJZ79RDHKS4GYXV29PM27DGCN0VB0BCZFF2FC6FMF3A6ZNKC238",
+ "key": "XMNYM62DQW0XDQACCYDMFTM5GY7SZST60NH7XS9GY18H8Q9N7QN0"
+ }
+ ],
+ "recoup": [],
+ "denoms": [
+ {
+ "master_sig": "2SDD44VVBD52XEV0A9R878BC60J51VKK0H5ZS6CPJ7Z738A8V4KPXCF70KFZAY2567400C2GEWNNVXF6PYD7HKX3D2M63WCNPJSE010",
+ "stamp_start": {
+ "t_ms": 1582484881000
+ },
+ "stamp_expire_withdraw": {
+ "t_ms": 1677092881000
+ },
+ "stamp_expire_deposit": {
+ "t_ms": 1740164881000
+ },
+ "stamp_expire_legal": {
+ "t_ms": 1897844881000
+ },
+ "denom_pub": "040000Z9TH9RPTA1BXF6Z89HM7JGXTPD5G8NNBWQWF7RWQGNAATN84QBWME1TGSWZ79WPQ62S2W2VHG2XBH66JDJ0KM8Q2FQ3FGBZQGNJVFNA9F66E6S3P36KTMWMKWDXWM9EX1YHSHQ841AHRR8JVDY96CZ13AJF6JW95K59AE8CSTH5ZS9NVS0102X92GK8JW2QX2S4EE25QNHK6XMXH3944QMXPFS7SFCMV623BM62VNPVX8JM424YXPJ09TXZAH2CF3QM5HDVRSTDRDGVBF6KZVRFM852TMVMYPVGFA9YQF6HWNJ8H5VCQ3Z9WWNMQ3T76X4F1P6W2J266K8B3W9HKW2WJNK3XHRAVC4GCF07TC0ZNAT0EDAAKV429YAXWSK952BPTY98GVP5XZQG2SE0Q5CF3PV04002",
+ "value": "TESTKUDOS:5",
+ "fee_withdraw": "TESTKUDOS:0.01",
+ "fee_deposit": "TESTKUDOS:0.01",
+ "fee_refresh": "TESTKUDOS:0.01",
+ "fee_refund": "TESTKUDOS:0.01"
+ },
+ {
+ "master_sig": "WX1RDTW1W1K7FPFQQ9MCJ8V5CJP8ZX2SGHQG2YS34BY8AMSR3YQ92HE2HT1JMP4W06J63RZ0BR2MKDBX54GV6QKD4R00N9HXN2S283R",
+ "stamp_start": {
+ "t_ms": 1582484881000
+ },
+ "stamp_expire_withdraw": {
+ "t_ms": 1677092881000
+ },
+ "stamp_expire_deposit": {
+ "t_ms": 1740164881000
+ },
+ "stamp_expire_legal": {
+ "t_ms": 1897844881000
+ },
+ "denom_pub": "040000XV91V0M7H906Y7R371YX2XAK1V5B2TRFD8ZM9WYJ495TP08NCVEDNFXS2KZBJR4808VZ52PNNQSYVQ2T3J7867MZQY1QZ9N8YQWQWCKSYAY8A07E5SYAK0G0KRTCN5VZ7JXE2YCNT7Q3RT9TGAZBSK5V1ZRRK6HX4C1YFKPWWP4TBVJ8DJMS43WKR4CR4S9T02YXVGR6GSDMR7GHBD89JHCEQ8V2K58Y5XVDGRRQYNBG9Q5XWDMV7GKN24JCPCEKSZZP5XYPXYJX2Z2JZ179M9FQV0PEWFJ4DP7AP14XE54FH97YP9398KA31ECVDY7PHMKMZ6E79Z9FSCXH3WSCXMHBWFGWPRG5ZG1P6HR71VKH4F0Y998JNE4G40JH2VSXP3035AR7HAMJGB56CYHH60EWD904002",
+ "value": "TESTKUDOS:2",
+ "fee_withdraw": "TESTKUDOS:0.01",
+ "fee_deposit": "TESTKUDOS:0.01",
+ "fee_refresh": "TESTKUDOS:0.01",
+ "fee_refund": "TESTKUDOS:0.01"
+ },
+ {
+ "master_sig": "65G9FWQPA4YKJEM7D37079D4MY81D47KD1280RG7BRH85XZQ2N13FJPV9N8AEASK9CTGNX1HKX0GTRBJ5C49H4YRY0E4CYVPNH06W18",
+ "stamp_start": {
+ "t_ms": 1582484881000
+ },
+ "stamp_expire_withdraw": {
+ "t_ms": 1677092881000
+ },
+ "stamp_expire_deposit": {
+ "t_ms": 1740164881000
+ },
+ "stamp_expire_legal": {
+ "t_ms": 1897844881000
+ },
+ "denom_pub": "040000XCBJE9TDDZATYSDR51D0DKMY5NW8FMJ8YQ1Y4F40SPPTKFMD3FWH38NSQZ1YB621TCFH5RBN5J3SFR5SG4789G27FA90E605GG9AXYTXXPJ9HYPAVAVS6V4XCSC17HKX2M2NSX5D0PPETDGKQD04G498VS36YY4WTB5SYG4SV9MKPVZ5WG2WNP3MA77TFZSHK5HBHZBEW0S1TRKGSCDNBRHYB240M84YM1Y7EJ7BXKJK4GRR1GS16DJ2RA1YEQ1AAXH0GP6RRAEJE8D2JFSH05P3KR1GB97NMX6VD8DCAX45416F888EYQR4M6R820FJVZ6FYV9CCMZ3M10B64N6G4QFNKFNAV2ENPVVG4A3R0AAA6STJ7E5Z05GEKW35SHM14HY9CEGM7D1ZEKHZJYA9P6WH504002",
+ "value": "TESTKUDOS:1",
+ "fee_withdraw": "TESTKUDOS:0.01",
+ "fee_deposit": "TESTKUDOS:0.01",
+ "fee_refresh": "TESTKUDOS:0.01",
+ "fee_refund": "TESTKUDOS:0.01"
+ },
+ {
+ "master_sig": "M423J7CJPACTPBYCFVR87B44JAJKAB2ME8C263WGHJSA8V8444SX428MVC9NF4GD08CKS9HY0WB4B8SEZ3HJFWKXNSH80RBJXQC822G",
+ "stamp_start": {
+ "t_ms": 1582484881000
+ },
+ "stamp_expire_withdraw": {
+ "t_ms": 1677092881000
+ },
+ "stamp_expire_deposit": {
+ "t_ms": 1740164881000
+ },
+ "stamp_expire_legal": {
+ "t_ms": 1897844881000
+ },
+ "denom_pub": "040000XZDZK4BPPPXR7MYKK2AF4WF95EH3VF8WEX7WDX4HEWXSB5XX10N4V5RHFSK0TSBKNC9CRNVGK3WJ42S3Z9SB4Q3M4DQQ7DKCGKED6WBKENHT8JX51K1VR5JKCMAFBNM6DR5MNRGKFC2MDRQ0Y4BCXHKEMRD65C6JPBKYW9HJH66FGT22WMBV0AV7P60CKR13MQG6FKWW3TZW3XXHVY2VX9MJN6VQFPS6NQGGTNXZV2SK2X5MJAJME7RN9BNZ5ZBTW1CYMVCHBSVGBFPRC68W78PW44VP402VD12KG2AWKPD4DRBAA85HM1DN1KADYQ498QHYGEB3T3HH990HRV8PSNBGYCHB87JTVYMJ4N2PSP2FCX0H6FRTW1FQY05EB7D8BFXM95DNRCHVQSHBZ9RP7NZFA304002",
+ "value": "TESTKUDOS:10",
+ "fee_withdraw": "TESTKUDOS:0.01",
+ "fee_deposit": "TESTKUDOS:0.01",
+ "fee_refresh": "TESTKUDOS:0.01",
+ "fee_refund": "TESTKUDOS:0.01"
+ },
+ {
+ "master_sig": "RKZKGR0KYKY0VZ26339DZKV8EZJ2HRRQMFSAJDHBG3YHEQNZFHKM8WPYCH9WHXTWBB10GQN9QJKFDJJF2H6D5FT801GF87G153PTJ18",
+ "stamp_start": {
+ "t_ms": 1582484881000
+ },
+ "stamp_expire_withdraw": {
+ "t_ms": 1677092881000
+ },
+ "stamp_expire_deposit": {
+ "t_ms": 1740164881000
+ },
+ "stamp_expire_legal": {
+ "t_ms": 1897844881000
+ },
+ "denom_pub": "040000YKYFF6GX979JS10MEZ16BQ7TT6XBTE0TBX6VJ9KSG7K4D91SWJVDETNKQJXAFK9SAB3S31FZFA0Y0X22TKRXKCT7Z4GZCCRJJ12T1A5M4DWRTZDFRD3FE495NXHVPFM96KXMKH1HABTDDFZN0NWQ3NBJ6GNXD40NJ95E955X948JHBDJZWM3TEAK4XFJX8056XFDHVNXSF4VN14RR1WD1J5K7JPS61SKRNF3HT6NZA823PZW2KPV2KVBMMP615A922ZNJGVQDTW5TYWTK5DCBGG1YEKQRYF39NX9X722FZK98BTMHHH6WZFCKBT096G9BKSHSJW3VE8KKPCN8XGWYYPD3158HRKSA28BJQ9XJVVB6FDCGZ154WWGGSGW82BDYDH7ZHJBMS046AG0ND4ZCVR2JQ04002",
+ "value": "TESTKUDOS:0.1",
+ "fee_withdraw": "TESTKUDOS:0.01",
+ "fee_deposit": "TESTKUDOS:0.01",
+ "fee_refresh": "TESTKUDOS:0.01",
+ "fee_refund": "TESTKUDOS:0.01"
+ },
+ {
+ "master_sig": "FJPQKECRKVQSTB9Y983KDGD65Z1JHQKNSCC6YPMBN3Z4VW0AGC5MQM9BPB0YYD1SCMETPD6QB4X80HWE0ZDGWNZB1KND5TP567T4G3G",
+ "stamp_start": {
+ "t_ms": 1582484881000
+ },
+ "stamp_expire_withdraw": {
+ "t_ms": 1677092881000
+ },
+ "stamp_expire_deposit": {
+ "t_ms": 1740164881000
+ },
+ "stamp_expire_legal": {
+ "t_ms": 1897844881000
+ },
+ "denom_pub": "040000Y9PBY1HPBDD4KSK9PBA86TRY13JQ4284T4E0G4SEREQ8YM88PZHKW1ACKT1RTWVTBXX83G54NFVYRJQX9PTDXDJ1CXSS42G8NYMW97NA6NNNASV69W1JX39NTS1NVKXPW4WMBASATSNBTXHRT92FFN2NAJFGK876BNN3TPTH57C76ADAQV43VFF7CYAWWNYZAYGQQ1XY1NK34FJD778VFGYCZ1G9J8XPNB92ZKJBZEZKSNBRNH27GM5A736AFSGP7B4JSCGD0F4FMD1PDVB26MM9ZK8C1TDKXQ5DJ09AQQ55P7Q3A133ASPGBH6SCJTJYH8C9A451B0SP4GDX2ZFRSX5FP93PY4VKEB36KCAQ5E2MRZNWFB6T0JK0W7Z7NXP5FW2VQ4PNV7B2NQ3WFMCVRSDSV04002",
+ "value": "TESTKUDOS:1000",
+ "fee_withdraw": "TESTKUDOS:0.01",
+ "fee_deposit": "TESTKUDOS:0.01",
+ "fee_refresh": "TESTKUDOS:0.01",
+ "fee_refund": "TESTKUDOS:0.01"
+ }
+ ],
+ "auditors": [],
+ "list_issue_date": {
+ "t_ms": 1592161681000
+ },
+ "eddsa_pub": "0FMRBH8FZYYMSQ2RHTYYGK2BV33JVSW6MTYCV7Y833GVNXFDYK10",
+ "eddsa_sig": "2GB384567SZM9CM7RJT51N04D2ZK7NAHWZRT6BA0FFNXTAB71D4T1WVQTXZEPDM07X1MJ46ZBC189SCM4EG4V8TQJRP2WAZCKPAJJ2R"
+ }""".trimIndent()
+ }
+ runCoroutine {
+ val keys = Keys.fetch(httpClient, "https://exchange.test.taler.net/")
+ assertEquals(expectedKeys, keys)
+ }
+ }
+
+
+} \ No newline at end of file
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/UpdateTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/UpdateTest.kt
new file mode 100644
index 0000000..271dc09
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/UpdateTest.kt
@@ -0,0 +1,37 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.exchange
+
+import net.taler.wallet.kotlin.runCoroutine
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+class UpdateTest {
+
+ private val exchange = Exchange()
+
+ @Ignore // live test that requires internet connectivity
+ @Test
+ fun testLiveUpdate() {
+ runCoroutine {
+ val record = exchange.updateFromUrl("http://exchange.test.taler.net/")
+ assertTrue(record.addComplete)
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/WireTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/WireTest.kt
new file mode 100644
index 0000000..d09b44b
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/WireTest.kt
@@ -0,0 +1,175 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.exchange
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.getMockHttpClient
+import net.taler.wallet.kotlin.giveJsonResponse
+import net.taler.wallet.kotlin.runCoroutine
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class WireTest {
+
+ private val httpClient = getMockHttpClient()
+
+ @Test
+ fun testFetchWire() {
+ val expectedWire = Wire(
+ accounts = listOf(
+ AccountInfo(
+ paytoUri = "payto://x-taler-bank/bank.test.taler.net/Exchange",
+ masterSig = "5DMYMQCEFWG7B21RAX8XQ585V689K8DSSR065F04E2JK6G9AF1WM8EVDCHHBMVWRY3P02EWEE4M6YVKPY6B43H2CPCWHDP13RM1WR10"
+ )
+ ),
+ fees = mapOf(
+ "x-taler-bank" to listOf(
+ WireFee(
+ wireFee = Amount.fromJSONString("TESTKUDOS:0.04"),
+ closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+ startStamp = Timestamp(1577833200000),
+ endStamp = Timestamp(1609455600000),
+ signature = "9DS6TXPTM8ZBBTJS9VCRSD9FVS56ZY9EVWCF4HDA3Y2DNWSVMGS7XXPWE295EZ3E98KVV1SWDJ11CP0A0VDSRDZTM6RD2RPRG19ZA2G"
+ ),
+ WireFee(
+ wireFee = Amount.fromJSONString("TESTKUDOS:0.04"),
+ closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+ startStamp = Timestamp(1609455600000),
+ endStamp = Timestamp(1640991600000),
+ signature = "81852REBNR3ZRQHKQ2FPT6CPACED0MA0CW4V9CPDS3NP2JX6X8BE5YE5W1AKR9XPASEXSKQH6FEXHP7VJB64XWA7FDGH5DKCD3Q700G"
+ ),
+ WireFee(
+ wireFee = Amount.fromJSONString("TESTKUDOS:0.05"),
+ closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+ startStamp = Timestamp(1640991600000),
+ endStamp = Timestamp(1672527600000),
+ signature = "REYMSGH4QBNF339Q8TD5VJMMY6BV7KFTC1Y69YD69Y9E8Z5HXGNAKCQKT490MHBSF48894YADT1ATGDMSRZAQJJFVXF6HX9JEYDT61G"
+ ),
+ WireFee(
+ wireFee = Amount.fromJSONString("TESTKUDOS:0.06"),
+ closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+ startStamp = Timestamp(1672527600000),
+ endStamp = Timestamp(1704063600000),
+ signature = "BXB47D936XT7XDHGA3VA3461CY1GMQWFPVMSBY01N5SN6PBCGYRS8HSY19FJ0P5HVX3TGS9TAHY9X7RP4BQHPM4DMMS30TJ0EKG5A3G"
+ ),
+ WireFee(
+ wireFee = Amount.fromJSONString("TESTKUDOS:0.07"),
+ closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+ startStamp = Timestamp(1704063600000),
+ endStamp = Timestamp(1735686000000),
+ signature = "RFF1KV54BH9TJ8KBE8YEY8DM0R468PZYGW82G16P97EDHNN3XZVN4KK5E9CBZQ730WPJT0RKR3TTYPBWGTR0YQ064XZZDHJHHZN1418"
+ ),
+ WireFee(
+ wireFee = Amount.fromJSONString("TESTKUDOS:0.08"),
+ closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+ startStamp = Timestamp(1735686000000),
+ endStamp = Timestamp(1767222000000),
+ signature = "Q89VKJ54MF3DVG0NKK4N6VB96NCT0PRSTNBJ0SSB42SQTHB10JC68XJSDM6PRBBPEJ8CHDE9VVRZWW20VFSZFDTRA332JKDSBBFWY1G"
+ )
+ )
+ )
+ )
+ httpClient.giveJsonResponse("https://exchange.test.taler.net/wire") {
+ """{
+ "accounts": [
+ {
+ "payto_uri": "payto://x-taler-bank/bank.test.taler.net/Exchange",
+ "master_sig": "5DMYMQCEFWG7B21RAX8XQ585V689K8DSSR065F04E2JK6G9AF1WM8EVDCHHBMVWRY3P02EWEE4M6YVKPY6B43H2CPCWHDP13RM1WR10"
+ }
+ ],
+ "fees": {
+ "x-taler-bank": [
+ {
+ "wire_fee": "TESTKUDOS:0.04",
+ "closing_fee": "TESTKUDOS:0.01",
+ "start_date": {
+ "t_ms": 1577833200000
+ },
+ "end_date": {
+ "t_ms": 1609455600000
+ },
+ "sig": "9DS6TXPTM8ZBBTJS9VCRSD9FVS56ZY9EVWCF4HDA3Y2DNWSVMGS7XXPWE295EZ3E98KVV1SWDJ11CP0A0VDSRDZTM6RD2RPRG19ZA2G"
+ },
+ {
+ "wire_fee": "TESTKUDOS:0.04",
+ "closing_fee": "TESTKUDOS:0.01",
+ "start_date": {
+ "t_ms": 1609455600000
+ },
+ "end_date": {
+ "t_ms": 1640991600000
+ },
+ "sig": "81852REBNR3ZRQHKQ2FPT6CPACED0MA0CW4V9CPDS3NP2JX6X8BE5YE5W1AKR9XPASEXSKQH6FEXHP7VJB64XWA7FDGH5DKCD3Q700G"
+ },
+ {
+ "wire_fee": "TESTKUDOS:0.05",
+ "closing_fee": "TESTKUDOS:0.01",
+ "start_date": {
+ "t_ms": 1640991600000
+ },
+ "end_date": {
+ "t_ms": 1672527600000
+ },
+ "sig": "REYMSGH4QBNF339Q8TD5VJMMY6BV7KFTC1Y69YD69Y9E8Z5HXGNAKCQKT490MHBSF48894YADT1ATGDMSRZAQJJFVXF6HX9JEYDT61G"
+ },
+ {
+ "wire_fee": "TESTKUDOS:0.06",
+ "closing_fee": "TESTKUDOS:0.01",
+ "start_date": {
+ "t_ms": 1672527600000
+ },
+ "end_date": {
+ "t_ms": 1704063600000
+ },
+ "sig": "BXB47D936XT7XDHGA3VA3461CY1GMQWFPVMSBY01N5SN6PBCGYRS8HSY19FJ0P5HVX3TGS9TAHY9X7RP4BQHPM4DMMS30TJ0EKG5A3G"
+ },
+ {
+ "wire_fee": "TESTKUDOS:0.07",
+ "closing_fee": "TESTKUDOS:0.01",
+ "start_date": {
+ "t_ms": 1704063600000
+ },
+ "end_date": {
+ "t_ms": 1735686000000
+ },
+ "sig": "RFF1KV54BH9TJ8KBE8YEY8DM0R468PZYGW82G16P97EDHNN3XZVN4KK5E9CBZQ730WPJT0RKR3TTYPBWGTR0YQ064XZZDHJHHZN1418"
+ },
+ {
+ "wire_fee": "TESTKUDOS:0.08",
+ "closing_fee": "TESTKUDOS:0.01",
+ "start_date": {
+ "t_ms": 1735686000000
+ },
+ "end_date": {
+ "t_ms": 1767222000000
+ },
+ "sig": "Q89VKJ54MF3DVG0NKK4N6VB96NCT0PRSTNBJ0SSB42SQTHB10JC68XJSDM6PRBBPEJ8CHDE9VVRZWW20VFSZFDTRA332JKDSBBFWY1G"
+ }
+ ]
+ },
+ "master_public_key": "DY95EXAHQ2BKM2WK9YHZHYG1R7PPMMJPY14FNGP662DAKE35AKQG"
+ }""".trimIndent()
+ }
+
+ runCoroutine {
+ val wire = Wire.fetch(httpClient, "https://exchange.test.taler.net/")
+ assertEquals(expectedWire, wire)
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/operations/WithdrawTest.kt b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/operations/WithdrawTest.kt
new file mode 100644
index 0000000..541f24f
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/wallet/kotlin/operations/WithdrawTest.kt
@@ -0,0 +1,140 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.operations
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.exchange.DenominationSelectionInfo
+import net.taler.wallet.kotlin.exchange.Denominations.denomination0d01
+import net.taler.wallet.kotlin.exchange.Denominations.denomination0d1
+import net.taler.wallet.kotlin.exchange.Denominations.denomination1
+import net.taler.wallet.kotlin.exchange.Denominations.denomination10
+import net.taler.wallet.kotlin.exchange.Denominations.denomination2
+import net.taler.wallet.kotlin.exchange.Denominations.denomination4
+import net.taler.wallet.kotlin.exchange.Denominations.denomination5
+import net.taler.wallet.kotlin.exchange.Denominations.denomination8
+import net.taler.wallet.kotlin.exchange.SelectedDenomination
+import net.taler.wallet.kotlin.getMockHttpClient
+import net.taler.wallet.kotlin.giveJsonResponse
+import net.taler.wallet.kotlin.operations.Withdraw.BankDetails
+import net.taler.wallet.kotlin.runCoroutine
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+internal class WithdrawTest {
+
+ private val httpClient = getMockHttpClient()
+ private val withdraw = Withdraw(httpClient)
+
+ @Ignore // live test that requires internet connectivity and a working exchange
+ @Test
+ fun testLiveUpdate() {
+ runCoroutine {
+ val withdraw = Withdraw() // use own instance without mocked HTTP client
+ val url = "http://exchange.test.taler.net/"
+ val amount = Amount("TESTKUDOS", 5, 0)
+ val details = withdraw.getWithdrawalDetails(url, amount)
+ assertEquals(url, details.exchange.baseUrl)
+ }
+ }
+
+ @Test
+ fun getBankWithdrawalInfo() {
+ val bankDetails = BankDetails(
+ amount = Amount.fromJSONString("TESTKUDOS:5"),
+ confirmTransferUrl = "https://bank.test.taler.net/confirm-withdrawal/9b51c1dd-db41-4b5f-97d9-1071d5dd8091",
+ extractedStatusUrl = "https://bank.test.taler.net/api/withdraw-operation/9b51c1dd-db41-4b5f-97d9-1071d5dd8091",
+ selectionDone = false,
+ senderPaytoUri = "payto://x-taler-bank/bank.test.taler.net/test",
+ suggestedExchange = "https://exchange.test.taler.net/",
+ transferDone = false,
+ wireTypes = listOf("x-taler-bank")
+ )
+ httpClient.giveJsonResponse(bankDetails.extractedStatusUrl) {
+ """{
+ "selection_done": ${bankDetails.selectionDone},
+ "transfer_done": ${bankDetails.transferDone},
+ "amount": "${bankDetails.amount.toJSONString()}",
+ "wire_types": ["${bankDetails.wireTypes[0]}"],
+ "sender_wire": "${bankDetails.senderPaytoUri}",
+ "suggested_exchange": "${bankDetails.suggestedExchange}",
+ "confirm_transfer_url": "${bankDetails.confirmTransferUrl}"
+ }""".trimIndent()
+ }
+ runCoroutine {
+ val details =
+ withdraw.getBankInfo("taler://withdraw/bank.test.taler.net/9b51c1dd-db41-4b5f-97d9-1071d5dd8091")
+ assertEquals(bankDetails, details)
+ }
+ }
+
+ @Test
+ fun testGetDenominationSelection() {
+ val allDenominations = listOf(
+ denomination0d1,
+ denomination0d01, // unsorted list to test sort as well
+ denomination10,
+ denomination8,
+ denomination5,
+ denomination4,
+ denomination2,
+ denomination1
+ )
+ // select denominations for 10 TESTKUDOS
+ val value10 = Amount(currency = "TESTKUDOS", value = 10, fraction = 0)
+ val expectedSelectionInfo10 = DenominationSelectionInfo(
+ totalCoinValue = Amount(currency = "TESTKUDOS", value = 9, fraction = 82000000),
+ totalWithdrawCost = Amount(currency = "TESTKUDOS", value = 9, fraction = 99000000),
+ selectedDenominations = listOf(
+ SelectedDenomination(1, denomination8),
+ SelectedDenomination(1, denomination1),
+ SelectedDenomination(8, denomination0d1),
+ SelectedDenomination(2, denomination0d01)
+ )
+ )
+ assertEquals(expectedSelectionInfo10, withdraw.getDenominationSelection(value10, allDenominations))
+
+ // select denominations for 5.5 TESTKUDOS
+ val value5d5 = Amount(currency = "TESTKUDOS", value = 5, fraction = 50000000)
+ val expectedSelectionInfo5d5 = DenominationSelectionInfo(
+ totalCoinValue = Amount(currency = "TESTKUDOS", value = 5, fraction = 42000000),
+ totalWithdrawCost = Amount(currency = "TESTKUDOS", value = 5, fraction = 49000000),
+ selectedDenominations = listOf(
+ SelectedDenomination(1, denomination5),
+ SelectedDenomination(4, denomination0d1),
+ SelectedDenomination(2, denomination0d01)
+ )
+ )
+ assertEquals(expectedSelectionInfo5d5, withdraw.getDenominationSelection(value5d5, allDenominations))
+
+ // select denominations for 23.42 TESTKUDOS
+ val value23d42 = Amount(currency = "TESTKUDOS", value = 23, fraction = 42000000)
+ val expectedSelectionInfo23d42 = DenominationSelectionInfo(
+ totalCoinValue = Amount(currency = "TESTKUDOS", value = 23, fraction = 31000000),
+ totalWithdrawCost = Amount(currency = "TESTKUDOS", value = 23, fraction = 42000000),
+ selectedDenominations = listOf(
+ SelectedDenomination(2, denomination10),
+ SelectedDenomination(1, denomination2),
+ SelectedDenomination(1, denomination1),
+ SelectedDenomination(3, denomination0d1),
+ SelectedDenomination(1, denomination0d01)
+ )
+ )
+ assertEquals(expectedSelectionInfo23d42, withdraw.getDenominationSelection(value23d42, allDenominations))
+ }
+
+}
diff --git a/wallet/src/jsMain/kotlin/net/taler/wallet/kotlin/Db.kt b/wallet/src/jsMain/kotlin/net/taler/wallet/kotlin/Db.kt
new file mode 100644
index 0000000..45cbfc3
--- /dev/null
+++ b/wallet/src/jsMain/kotlin/net/taler/wallet/kotlin/Db.kt
@@ -0,0 +1,23 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+internal actual class DbFactory {
+ actual fun openDb(): Db {
+ return FakeDb()
+ }
+}
diff --git a/wallet/src/jsMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt b/wallet/src/jsMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
new file mode 100644
index 0000000..e428f6a
--- /dev/null
+++ b/wallet/src/jsMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
@@ -0,0 +1,185 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import org.khronos.webgl.Uint8Array
+import org.khronos.webgl.get
+
+internal actual object CryptoFactory {
+ internal actual fun getCrypto(): Crypto = CryptoJsImpl
+}
+
+internal object CryptoJsImpl : CryptoImpl() {
+
+ override fun sha256(input: ByteArray): ByteArray {
+ return hash.sha256().update(input.toUint8Array()).digest().toByteArray()
+ }
+
+ override fun sha512(input: ByteArray): ByteArray {
+ return nacl.hash(input.toUint8Array()).toByteArray()
+ }
+
+ override fun getHashSha512State(): HashSha512State {
+ return JsHashSha512State()
+ }
+
+ override fun getRandomBytes(num: Int): ByteArray {
+ return nacl.randomBytes(num).toByteArray()
+ }
+
+ override fun eddsaGetPublic(eddsaPrivateKey: ByteArray): ByteArray {
+ val pair = nacl.sign.keyPair.fromSeed(eddsaPrivateKey.toUint8Array())
+ return pair.publicKey.toByteArray()
+ }
+
+ override fun ecdheGetPublic(ecdhePrivateKey: ByteArray): ByteArray {
+ return nacl.scalarMult.base(ecdhePrivateKey.toUint8Array()).toByteArray()
+ }
+
+ override fun createEddsaKeyPair(): EddsaKeyPair {
+ val privateKey = nacl.randomBytes(32).toByteArray()
+ val publicKey = eddsaGetPublic(privateKey)
+ return EddsaKeyPair(privateKey, publicKey)
+ }
+
+ override fun createEcdheKeyPair(): EcdheKeyPair {
+ val privateKey = nacl.randomBytes(32).toByteArray()
+ val publicKey = ecdheGetPublic(privateKey)
+ return EcdheKeyPair(privateKey, publicKey)
+ }
+
+ override fun eddsaSign(msg: ByteArray, eddsaPrivateKey: ByteArray): ByteArray {
+ val privateKey = nacl.sign.keyPair.fromSeed(eddsaPrivateKey.toUint8Array()).secretKey
+ return nacl.sign.detached(msg.toUint8Array(), privateKey).toByteArray()
+ }
+
+ override fun eddsaVerify(msg: ByteArray, sig: ByteArray, eddsaPub: ByteArray): Boolean {
+ return nacl.sign.detached.verify(msg.toUint8Array(), sig.toUint8Array(), eddsaPub.toUint8Array())
+ }
+
+ override fun keyExchangeEddsaEcdhe(eddsaPrivateKey: ByteArray, ecdhePublicKey: ByteArray): ByteArray {
+ val ph = sha512(eddsaPrivateKey)
+ val a = ph.copyOfRange(0, 32)
+ val x = nacl.scalarMult(a.toUint8Array(), ecdhePublicKey.toUint8Array()).toByteArray()
+ return sha512(x)
+ }
+
+ override fun keyExchangeEcdheEddsa(ecdhePrivateKey: ByteArray, eddsaPublicKey: ByteArray): ByteArray {
+ val curve25519Pub =
+ ed2curve.convertPublicKey(eddsaPublicKey.toUint8Array()) ?: throw Error("invalid public key")
+ val x = nacl.scalarMult(ecdhePrivateKey.toUint8Array(), curve25519Pub).toByteArray()
+ return sha512(x)
+ }
+
+ override fun rsaBlind(hm: ByteArray, bks: ByteArray, rsaPubEnc: ByteArray): ByteArray {
+ TODO("Not yet implemented")
+ }
+
+ override fun rsaUnblind(sig: ByteArray, rsaPubEnc: ByteArray, bks: ByteArray): ByteArray {
+ TODO("Not yet implemented")
+ }
+
+ override fun rsaVerify(hm: ByteArray, rsaSig: ByteArray, rsaPubEnc: ByteArray): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ private class JsHashSha512State : HashSha512State {
+ private val state = hash.sha512()
+
+ override fun update(data: ByteArray): HashSha512State {
+ state.update(data.toUint8Array())
+ return this
+ }
+
+ override fun final(): ByteArray {
+ return state.digest().toByteArray()
+ }
+ }
+
+ private fun Uint8Array.toByteArray(): ByteArray {
+ val result = ByteArray(this.length)
+ for (i in 0 until this.length) result[i] = this[i]
+ return result
+ }
+
+ private fun ByteArray.toUint8Array(): Uint8Array {
+ return Uint8Array(this.toTypedArray())
+ }
+
+}
+
+@Suppress("ClassName")
+@JsModule("tweetnacl")
+@JsNonModule
+private external class nacl {
+
+ companion object {
+ fun hash(input: Uint8Array): Uint8Array
+ fun scalarMult(n: Uint8Array, p: Uint8Array): Uint8Array
+ fun randomBytes(n: Int): Uint8Array
+ }
+
+ class scalarMult {
+ companion object {
+ fun base(n: Uint8Array): Uint8Array
+ }
+ }
+
+ class sign {
+ companion object {
+ fun detached(msg: Uint8Array, secretKey: Uint8Array): Uint8Array
+ }
+
+ class detached {
+ companion object {
+ fun verify(msg: Uint8Array, sig: Uint8Array, publicKey: Uint8Array): Boolean
+ }
+ }
+
+ class keyPair {
+ companion object {
+ fun fromSeed(seed: Uint8Array): KeyPair
+ }
+ }
+ }
+}
+
+private class KeyPair(val publicKey: Uint8Array, @Suppress("unused") val secretKey: Uint8Array)
+
+@Suppress("ClassName")
+@JsModule("ed2curve")
+@JsNonModule
+private external class ed2curve {
+ companion object {
+ fun convertPublicKey(pk: Uint8Array): Uint8Array?
+ }
+}
+
+@Suppress("ClassName")
+@JsModule("hash.js")
+@JsNonModule
+private external class hash {
+ class sha256 {
+ fun update(message: Uint8Array): sha256
+ fun digest(): Uint8Array
+ }
+
+ class sha512 {
+ fun update(message: Uint8Array): sha512
+ fun digest(): Uint8Array
+ }
+}
diff --git a/wallet/src/jsTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt b/wallet/src/jsTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
new file mode 100644
index 0000000..49466e0
--- /dev/null
+++ b/wallet/src/jsTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
@@ -0,0 +1,25 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.promise
+
+actual fun runCoroutine(block: suspend (scope : CoroutineScope) -> Unit): dynamic = GlobalScope.promise { block(this) }
+
+actual fun getPlatformTarget(): PlatformTarget = PlatformTarget.JS
diff --git a/wallet/src/nativeInterop/cinterop/sodium-static.def b/wallet/src/nativeInterop/cinterop/sodium-static.def
new file mode 100644
index 0000000..7000cbb
--- /dev/null
+++ b/wallet/src/nativeInterop/cinterop/sodium-static.def
@@ -0,0 +1,6 @@
+headers = sodium.h
+staticLibraries = libsodium.a
+libraryPaths = /usr/local/lib /usr/lib64 /usr/lib/x86_64-linux-gnu
+headerFilter = sodium.h sodium/**
+compilerOpts = -I/usr/include -I/usr/local/include -I/usr/include/x86_64-linux-gnu/
+linkerOpts = -lsodium -L/usr/lib/x86_64-linux-gnu -L/usr/lib64/ -L/usr/local/lib
diff --git a/wallet/src/nativeInterop/cinterop/sodium.def b/wallet/src/nativeInterop/cinterop/sodium.def
new file mode 100644
index 0000000..1c90295
--- /dev/null
+++ b/wallet/src/nativeInterop/cinterop/sodium.def
@@ -0,0 +1,4 @@
+headers = sodium.h
+headerFilter = sodium.h sodium/**
+compilerOpts = -I/usr/include -I/usr/local/include -I/usr/include/x86_64-linux-gnu/
+linkerOpts = -lsodium -L/usr/lib/x86_64-linux-gnu -L/usr/lib64/ -L/usr/local/lib
diff --git a/wallet/src/nativeMain/kotlin/net/taler/wallet/kotlin/Db.kt b/wallet/src/nativeMain/kotlin/net/taler/wallet/kotlin/Db.kt
new file mode 100644
index 0000000..45cbfc3
--- /dev/null
+++ b/wallet/src/nativeMain/kotlin/net/taler/wallet/kotlin/Db.kt
@@ -0,0 +1,23 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+internal actual class DbFactory {
+ actual fun openDb(): Db {
+ return FakeDb()
+ }
+}
diff --git a/wallet/src/nativeMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt b/wallet/src/nativeMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
new file mode 100644
index 0000000..7860607
--- /dev/null
+++ b/wallet/src/nativeMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
@@ -0,0 +1,187 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import kotlinx.cinterop.CValuesRef
+import kotlinx.cinterop.UByteVar
+import kotlinx.cinterop.alloc
+import kotlinx.cinterop.free
+import kotlinx.cinterop.nativeHeap
+import kotlinx.cinterop.ptr
+import kotlinx.cinterop.refTo
+import org.libsodium.crypto_hash_sha256
+import org.libsodium.crypto_hash_sha256_bytes
+import org.libsodium.crypto_hash_sha512
+import org.libsodium.crypto_hash_sha512_bytes
+import org.libsodium.crypto_hash_sha512_final
+import org.libsodium.crypto_hash_sha512_init
+import org.libsodium.crypto_hash_sha512_state
+import org.libsodium.crypto_hash_sha512_update
+import org.libsodium.crypto_scalarmult
+import org.libsodium.crypto_scalarmult_BYTES
+import org.libsodium.crypto_scalarmult_base
+import org.libsodium.crypto_scalarmult_curve25519_BYTES
+import org.libsodium.crypto_sign_BYTES
+import org.libsodium.crypto_sign_PUBLICKEYBYTES
+import org.libsodium.crypto_sign_SECRETKEYBYTES
+import org.libsodium.crypto_sign_detached
+import org.libsodium.crypto_sign_ed25519_pk_to_curve25519
+import org.libsodium.crypto_sign_seed_keypair
+import org.libsodium.crypto_sign_verify_detached
+import org.libsodium.randombytes
+
+internal actual object CryptoFactory {
+ internal actual fun getCrypto(): Crypto = CryptoNativeImpl
+}
+
+@OptIn(ExperimentalUnsignedTypes::class)
+internal object CryptoNativeImpl : CryptoImpl() {
+
+ override fun sha256(input: ByteArray): ByteArray {
+ val output = ByteArray(crypto_hash_sha256_bytes().toInt())
+ val cInput = if (input.isEmpty()) null else input.toCValuesRef()
+ crypto_hash_sha256(output.toCValuesRef(), cInput, input.size.toULong())
+ return output
+ }
+
+ override fun sha512(input: ByteArray): ByteArray {
+ val output = ByteArray(crypto_hash_sha512_bytes().toInt())
+ val cInput = if (input.isEmpty()) null else input.toCValuesRef()
+ crypto_hash_sha512(output.toCValuesRef(), cInput, input.size.toULong())
+ return output
+ }
+
+ override fun getHashSha512State(): HashSha512State {
+ return NativeHashSha512State()
+ }
+
+ override fun getRandomBytes(num: Int): ByteArray {
+ val bytes = ByteArray(num)
+ randombytes(bytes.toCValuesRef(), num.toULong())
+ return bytes
+ }
+
+ override fun eddsaGetPublic(eddsaPrivateKey: ByteArray): ByteArray {
+ val publicKey = ByteArray(crypto_sign_PUBLICKEYBYTES.toInt())
+ val privateKey = ByteArray(crypto_sign_SECRETKEYBYTES.toInt())
+ crypto_sign_seed_keypair(publicKey.toCValuesRef(), privateKey.toCValuesRef(), eddsaPrivateKey.toCValuesRef())
+ return publicKey
+ }
+
+ override fun ecdheGetPublic(ecdhePrivateKey: ByteArray): ByteArray {
+ val publicKey = ByteArray(crypto_scalarmult_BYTES.toInt())
+ crypto_scalarmult_base(publicKey.toCValuesRef(), ecdhePrivateKey.toCValuesRef())
+ return publicKey
+ }
+
+ override fun createEddsaKeyPair(): EddsaKeyPair {
+ val privateKey = ByteArray(crypto_sign_SECRETKEYBYTES.toInt())
+ randombytes(privateKey.toCValuesRef(), crypto_sign_SECRETKEYBYTES.toULong())
+ val publicKey = eddsaGetPublic(privateKey)
+ return EddsaKeyPair(privateKey, publicKey)
+ }
+
+ override fun createEcdheKeyPair(): EcdheKeyPair {
+ val privateKey = ByteArray(crypto_scalarmult_BYTES.toInt())
+ randombytes(privateKey.toCValuesRef(), crypto_scalarmult_BYTES.toULong())
+ val publicKey = ecdheGetPublic(privateKey)
+ return EcdheKeyPair(privateKey, publicKey)
+ }
+
+ override fun eddsaSign(msg: ByteArray, eddsaPrivateKey: ByteArray): ByteArray {
+ val publicKey = ByteArray(crypto_sign_PUBLICKEYBYTES.toInt())
+ val privateKey = ByteArray(crypto_sign_SECRETKEYBYTES.toInt())
+ crypto_sign_seed_keypair(publicKey.toCValuesRef(), privateKey.toCValuesRef(), eddsaPrivateKey.toCValuesRef())
+
+ val signatureBytes = ByteArray(crypto_sign_BYTES.toInt())
+ crypto_sign_detached(
+ signatureBytes.toCValuesRef(),
+ null,
+ msg.toCValuesRef(),
+ msg.size.toULong(),
+ privateKey.toCValuesRef()
+ )
+ return signatureBytes
+ }
+
+ override fun eddsaVerify(msg: ByteArray, sig: ByteArray, eddsaPub: ByteArray): Boolean {
+ return crypto_sign_verify_detached(
+ sig.toCValuesRef(),
+ msg.toCValuesRef(),
+ msg.size.toULong(),
+ eddsaPub.toCValuesRef()
+ ) == 0
+ }
+
+ override fun keyExchangeEddsaEcdhe(eddsaPrivateKey: ByteArray, ecdhePublicKey: ByteArray): ByteArray {
+ val privateKey = sha512(eddsaPrivateKey).copyOfRange(0, 32)
+ val sharedKey = ByteArray(crypto_scalarmult_BYTES.toInt())
+ crypto_scalarmult(sharedKey.toCValuesRef(), privateKey.toCValuesRef(), ecdhePublicKey.toCValuesRef())
+ return sha512(sharedKey)
+ }
+
+ override fun keyExchangeEcdheEddsa(ecdhePrivateKey: ByteArray, eddsaPublicKey: ByteArray): ByteArray {
+ val curve25519Pub = ByteArray(crypto_scalarmult_curve25519_BYTES.toInt())
+ val cCurve25519Pub = curve25519Pub.toCValuesRef()
+ crypto_sign_ed25519_pk_to_curve25519(cCurve25519Pub, eddsaPublicKey.toCValuesRef())
+
+ val sharedKey = ByteArray(crypto_scalarmult_BYTES.toInt())
+ crypto_scalarmult(sharedKey.toCValuesRef(), ecdhePrivateKey.toCValuesRef(), cCurve25519Pub)
+ return sha512(sharedKey)
+ }
+
+ override fun rsaBlind(hm: ByteArray, bks: ByteArray, rsaPubEnc: ByteArray): ByteArray {
+ TODO("Not yet implemented")
+ }
+
+ override fun rsaUnblind(sig: ByteArray, rsaPubEnc: ByteArray, bks: ByteArray): ByteArray {
+ TODO("Not yet implemented")
+ }
+
+ override fun rsaVerify(hm: ByteArray, rsaSig: ByteArray, rsaPubEnc: ByteArray): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ private class NativeHashSha512State : HashSha512State {
+ private val state = nativeHeap.alloc<crypto_hash_sha512_state>()
+ private val statePointer = state.ptr
+
+ init {
+ check(crypto_hash_sha512_init(statePointer) == 0) { "Error doing crypto_hash_sha512_init" }
+ }
+
+ override fun update(data: ByteArray): HashSha512State {
+ val cInput = if (data.isEmpty()) null else data.toCValuesRef()
+ crypto_hash_sha512_update(statePointer, cInput, data.size.toULong())
+ return this
+ }
+
+ override fun final(): ByteArray {
+ val output = ByteArray(crypto_hash_sha512_bytes().toInt())
+ crypto_hash_sha512_final(statePointer, output.toCValuesRef())
+ nativeHeap.free(statePointer)
+ return output
+ }
+
+ }
+
+ private fun ByteArray.toCValuesRef(): CValuesRef<UByteVar> {
+ @Suppress("UNCHECKED_CAST")
+ return this.refTo(0) as CValuesRef<UByteVar>
+ }
+
+}
diff --git a/wallet/src/nativeTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt b/wallet/src/nativeTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
new file mode 100644
index 0000000..c44a846
--- /dev/null
+++ b/wallet/src/nativeTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
@@ -0,0 +1,24 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.runBlocking
+
+actual fun runCoroutine(block: suspend (scope : CoroutineScope) -> Unit) = runBlocking { block(this) }
+
+actual fun getPlatformTarget(): PlatformTarget = PlatformTarget.NATIVE