diff options
author | Torsten Grote <t@grobox.de> | 2020-08-14 10:05:51 -0300 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2020-08-14 10:12:53 -0300 |
commit | 2ac13b19a5c7fc3531447333fe1772a78ca35795 (patch) | |
tree | d5579d87392a6a36d88deec4ceac777e68d6e8b8 /wallet/src/commonTest/kotlin/net/taler/wallet/kotlin | |
parent | a35337b991f8b3d38c3480fb76acd9fc6df47885 (diff) | |
download | wallet-kotlin-2ac13b19a5c7fc3531447333fe1772a78ca35795.tar.gz wallet-kotlin-2ac13b19a5c7fc3531447333fe1772a78ca35795.tar.bz2 wallet-kotlin-2ac13b19a5c7fc3531447333fe1772a78ca35795.zip |
Add Taler common multiplatform library to project
Diffstat (limited to 'wallet/src/commonTest/kotlin/net/taler/wallet/kotlin')
23 files changed, 3134 insertions, 0 deletions
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)) + } + +} |