diff options
author | Torsten Grote <t@grobox.de> | 2020-07-02 13:41:34 -0300 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2020-07-02 13:41:34 -0300 |
commit | 4a712e1da5bb66d7a7e27271e65e09d4910e2339 (patch) | |
tree | 10221c1edda8ee2d788f66abeb1cc52ed74e5151 | |
parent | 08f5f3618b5a9d9f38f7da0dbff39164c8b5f77b (diff) | |
download | wallet-kotlin-4a712e1da5bb66d7a7e27271e65e09d4910e2339.tar.gz wallet-kotlin-4a712e1da5bb66d7a7e27271e65e09d4910e2339.tar.bz2 wallet-kotlin-4a712e1da5bb66d7a7e27271e65e09d4910e2339.zip |
Add refresh crypto with tests
5 files changed, 743 insertions, 3 deletions
diff --git a/build.gradle b/build.gradle index 8ae318e..2dce365 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,7 @@ kotlin { commonMain { dependencies { implementation kotlin('stdlib-common') + implementation "com.soywiz.korlibs.klock:klock:1.11.12" } } commonTest { diff --git a/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt b/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt index 6158c52..715bf83 100644 --- a/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt +++ b/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt @@ -1,10 +1,10 @@ package net.taler.wallet.kotlin.crypto import java.math.BigInteger +import kotlin.math.abs import kotlin.math.ceil import kotlin.math.floor -@OptIn(ExperimentalStdlibApi::class) internal object RsaBlinding { fun rsaBlind(hm: ByteArray, bks: ByteArray, rsaPubEnc: ByteArray): ByteArray { @@ -40,8 +40,8 @@ internal object RsaBlinding { } private fun rsaPubDecode(publicKey: ByteArray): RsaPublicKey { - val modulusLength = (publicKey[0].toInt() shl 8) or publicKey[1].toInt() - val exponentLength = (publicKey[2].toInt() shl 8) or publicKey[3].toInt() + val modulusLength = abs((publicKey[0].toInt() shl 8) or publicKey[1].toInt()) + val exponentLength = abs((publicKey[2].toInt() shl 8) or publicKey[3].toInt()) if (4 + exponentLength + modulusLength != publicKey.size) { throw Error("invalid RSA public key (format wrong)") } diff --git a/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt b/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt new file mode 100644 index 0000000..37cf10f --- /dev/null +++ b/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt @@ -0,0 +1,481 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.kotlin.crypto + +import net.taler.wallet.kotlin.Amount +import net.taler.wallet.kotlin.Base32Crockford +import net.taler.wallet.kotlin.CoinRecord +import net.taler.wallet.kotlin.CoinSourceType.WITHDRAW +import net.taler.wallet.kotlin.CoinStatus.DORMANT +import net.taler.wallet.kotlin.DenominationRecord +import net.taler.wallet.kotlin.DenominationStatus +import net.taler.wallet.kotlin.Timestamp +import net.taler.wallet.kotlin.crypto.Refresh.DenominationSelectionInfo +import net.taler.wallet.kotlin.crypto.Refresh.RefreshPlanchetRecord +import net.taler.wallet.kotlin.crypto.Refresh.RefreshSessionRecord +import net.taler.wallet.kotlin.crypto.Refresh.SelectedDenomination +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +// TODO move to commonTest once RsaBlinding is implemented everywhere +class RefreshTest { + + private val crypto = CryptoFactory.getCrypto() + private val refresh = Refresh(crypto) + + private class RefreshVector( + val kappa: Int, + val meltCoin: CoinRecord, + val newCoinDenominations: DenominationSelectionInfo, + val meltFee: Amount, + val refreshSessionRecord: RefreshSessionRecord, + val kappaKeys: List<EcdheKeyPair> + ) + + @Test + fun testCreateRefreshSession() { + val vectors = listOf( + RefreshVector( + kappa = 3, + meltCoin = CoinRecord( + blindingKey = "N2A8K8XVTMPKNRCEJ1B8GMWJM6TXWRGAN5HWPFGPXS3WY76EKB60", + coinPriv = "884XH9DBQT9MA34YKCR7WPEP2HH3J1R9C3G7MSB8C3NHB0XEVZW0", + coinPub = "3GSHK9JCTMEAZY7BMC1QQD28ABEJEJJJXHPVF2YXTET97WB81GNG", + currentAmount = Amount("TESTKUDOS", 1, 96000000), + denomPub = "020000YDCXEZRKA0RR18PBW1HGXAH9HM9PN5TPWSVB8TG9QP4KDNHDVS0BRM9ATZV5AW5238WSNJWXA7V6WHE5QGMKRTSZYZZ6DWNHMZGAKCBC3T1HWKVPBHTRJZK68CGRQQ7A6VBRPCRS26QP8Z89TFRVN9NAQC6ZR0CD6P269XNQXYQAARNVBB74QSHJJXPMB5HE6TQ1HHK6AZRNWG2001", + denomPubHash = "9ZSE6E0271SRAZTHS08BYF8TGAY2Y1Q8Y7TYWM3ZM25THE3ZJYNKDCXR72YKVQSES1GXWGQVSF13YZ0Z2RQE5VRBMF79KJQP85R71X8", + denomSig = "73XS5TEM2WNQWJJBBMYC77DCV98NKX949KAFNJX9KXW2H9QDE4FM7SEJ7RB6RHX99QJGVT6SD1N24GXQCJY1CZA57RKDP3DK2C9WJ8HZ09Y33MYV33TD7C325GQQJFSSZ1EC5JX06BYNGKB0VEHXAR7KHXVT0Y5MZXSD8X65GRNPFV8B3HKC6BBNGVVXXM28WA7VTA7EWJKM4", + exchangeBaseUrl = "example.org", status = DORMANT, coinSource = WITHDRAW, suspended = false + ), + newCoinDenominations = DenominationSelectionInfo( + totalCoinValue = Amount("TESTKUDOS", 1, 81000000), + totalWithdrawCost = Amount("TESTKUDOS", 1, 92000000), + selectedDenominations = listOf( + SelectedDenomination( + count = 1, + denominationRecord = DenominationRecord( + denomPub = "020000YDW0GZQGY9GSKTEESAD1ZS803D576X1HTVJM8CWEBPVGQ4GHMDD5SBQHJ462NPW9FJD437HYW69MJR5N4YABJWYZB6P7CZ7CZR0YD7KY1M7C291BQX4T18DGKCTDEDBMVH2CF4K4NWVA1FFX4AYDB5GRWJ0JBJRRAVQ1ZY8D9PX9TJT9HEVBXAMQQF2ESKG1JX5CXDX7AMY2TG2001", + denomPubHash = "ZVZMM6GEFETH1S71BRKFKB03BTSTA8JZH26Q1PZED78NZ50EVVT0DMDBPDN5EBY90SK4AWT0J8CNPD7NB2TSCFMSKAS5V6DK26GEKT0", + exchangeBaseUrl = "example.org", + feeDeposit = Amount("TESTKUDOS", fraction = 2000000, value = 0), + feeRefresh = Amount("TESTKUDOS", fraction = 3000000, value = 0), + feeRefund = Amount("TESTKUDOS", fraction = 1000000, value = 0), + feeWithdraw = Amount("TESTKUDOS", fraction = 2000000, value = 0), + isOffered = true, + isRevoked = false, + masterSig = "1XDNXJXA9CY30Q5BTJW5YN8XSYCCFKKSVGR9TW5CFPD4YSTYY52YBHDEPEAX3AY1HWR0XHYQHRJCX156NZMAPQ4BPZ00EXTW0MA1018", + stampExpireDeposit = Timestamp(1656768984000), + stampExpireLegal = Timestamp(1688304984000), + stampExpireWithdraw = Timestamp(1594301784000), + stampStart = Timestamp(1593696984000), + status = DenominationStatus.VerifiedGood, + value = Amount("TESTKUDOS", fraction = 0, value = 1) + ) + ), + SelectedDenomination( + count = 8, + denominationRecord = DenominationRecord( + denomPub = "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001", + denomPubHash = "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R", + exchangeBaseUrl = "example.org", + feeDeposit = Amount("TESTKUDOS", fraction = 1000000, value = 0), + feeRefresh = Amount("TESTKUDOS", fraction = 3000000, value = 0), + feeRefund = Amount("TESTKUDOS", fraction = 1000000, value = 0), + feeWithdraw = Amount("TESTKUDOS", fraction = 1000000, value = 0), + isOffered = true, + isRevoked = false, + masterSig = "X4WYKHZVAQFRXKCAQ0QCA8VFMC32ESJP8M3BTTYW20SBR46Q1HRF9Y309SQVVH8TY3R8PJ81A4QFAABYEQR2D9RXTSVCBMD667Q723R", + stampExpireDeposit = Timestamp(1656768984000), + stampExpireLegal = Timestamp(1688304984000), + stampExpireWithdraw = Timestamp(1594301784000), + stampStart = Timestamp(1593696984000), + status = DenominationStatus.VerifiedGood, + value = Amount("TESTKUDOS", fraction = 10000000, value = 0) + ) + ), + SelectedDenomination( + count = 1, + denominationRecord = DenominationRecord( + denomPub = "020000Z46ESSPKMQ5875ZZMX9EZ4ZD7MQ3MPM4R2QJ7K7ADMCK0CJDM7W6GEZF0BQGWHVCMA4CY83NBYKBZYB9AYG293JD7VW7BSQTFH51HK02Q96JRTXBAY2DS8ZEXQFXRM6SD43C6BJCBDRCSXXEXA1WG2FFG5CX65TZW94274CMHJNVAY6VR8Y8XKV928P7AHBK106R6TCTS164CG2001", + denomPubHash = "0F44ZC9M6XB828XW09DY2YZNM07DA0CZV72D69F31RXH1CQHF3GDPVGZT5135BJEHVX0RGN44SCWS8CR8E39TDSRW10GZ6AEB2R40E8", + exchangeBaseUrl = "example.org", + feeDeposit = Amount("TESTKUDOS", fraction = 1000000, value = 0), + feeRefresh = Amount("TESTKUDOS", fraction = 1000000, value = 0), + feeRefund = Amount("TESTKUDOS", fraction = 1000000, value = 0), + feeWithdraw = Amount("TESTKUDOS", fraction = 1000000, value = 0), + isOffered = true, + isRevoked = false, + masterSig = "8DGF50VWCB2JHM613NRJM0NQYAVWFXCSBAHKTG3ZHKQ7N59WXK7MA4V794A97YDG0AVP350B2NT1QQ66VS1H2QG3P29PM5QDE6ZH60R", + stampExpireDeposit = Timestamp(1656768984000), + stampExpireLegal = Timestamp(1688304984000), + stampExpireWithdraw = Timestamp(1594301784000), + stampStart = Timestamp(1593696984000), + status = DenominationStatus.VerifiedGood, + value = Amount("TESTKUDOS", fraction = 1000000, value = 0) + ) + ) + ) + ), + meltFee = Amount("TESTKUDOS", fraction = 3000000, value = 0), + refreshSessionRecord = RefreshSessionRecord( + confirmSig = "97N1DQ1FGXT6SFR4J8H9WXCNG741BE986E3YW59T6R4DZ1FPC3XRFQ9D6BNRHGTAANZQXH4T1KXH61DP8BGHY6SYHZJWVGSQBV0A00G", + exchangeBaseUrl = "example.org", + hash = "XCSSAYNJ6964PVG393PDEGX0CGGZBNY6YMRDNZYBCTBDJF8ASA0BEA5TK435GG1DCFTTK26SS3V0EQ0DB9XKC9M9PWHTE4SZK9JZPVG", + meltCoinPub = "3GSHK9JCTMEAZY7BMC1QQD28ABEJEJJJXHPVF2YXTET97WB81GNG", + newDenominationHashes = listOf( + "ZVZMM6GEFETH1S71BRKFKB03BTSTA8JZH26Q1PZED78NZ50EVVT0DMDBPDN5EBY90SK4AWT0J8CNPD7NB2TSCFMSKAS5V6DK26GEKT0", + "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R", + "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R", + "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R", + "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R", + "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R", + "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R", + "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R", + "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R", + "0F44ZC9M6XB828XW09DY2YZNM07DA0CZV72D69F31RXH1CQHF3GDPVGZT5135BJEHVX0RGN44SCWS8CR8E39TDSRW10GZ6AEB2R40E8" + ), + newDenominations = listOf( + "020000YDW0GZQGY9GSKTEESAD1ZS803D576X1HTVJM8CWEBPVGQ4GHMDD5SBQHJ462NPW9FJD437HYW69MJR5N4YABJWYZB6P7CZ7CZR0YD7KY1M7C291BQX4T18DGKCTDEDBMVH2CF4K4NWVA1FFX4AYDB5GRWJ0JBJRRAVQ1ZY8D9PX9TJT9HEVBXAMQQF2ESKG1JX5CXDX7AMY2TG2001", + "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001", + "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001", + "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001", + "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001", + "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001", + "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001", + "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001", + "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001", + "020000Z46ESSPKMQ5875ZZMX9EZ4ZD7MQ3MPM4R2QJ7K7ADMCK0CJDM7W6GEZF0BQGWHVCMA4CY83NBYKBZYB9AYG293JD7VW7BSQTFH51HK02Q96JRTXBAY2DS8ZEXQFXRM6SD43C6BJCBDRCSXXEXA1WG2FFG5CX65TZW94274CMHJNVAY6VR8Y8XKV928P7AHBK106R6TCTS164CG2001" + ), + noRevealIndex = null, + planchetsForGammas = listOf( + listOf( + RefreshPlanchetRecord( + blindingKey = "G17FNHNV8BA7BFZRG75PPKJSK7FC47GCXF8YKJZ491D115FQEGE0", + coinEv = + "85BB17VN2XV7TTNKV5MS64JVA7X39TMCSQQQKWFT30SZFJDA4A9F06SGP89GF7ZZBAVB8X3VTT94SKRQ192ENJS4K12715NKHVH11XWVKND063AK16ANGV2P3XCP32Y4GRJGZMZ068EQS623C4CW75VB2KPF45TKEJ3VX4M4GXWMB3H07F8NQDQGMPFCBDS8SG2HXESC8DXVE", + privateKey = "972E3RE0GFMMGXHZTYSFD0H3YDYGGBNH80F5GMHPCN7C8NQCH460", + publicKey = "BH1Y1VBKNH86RT9FD191MP82W7PAQCA0SN9GPBN23N02M9NBYS50" + ), + RefreshPlanchetRecord( + blindingKey = "QR15AD5SFG20S9KW70P1MFAGPQC9WK7MSD0VFQXHKPDZTVZ6BZV0", + coinEv = + "DBKVPSHR1N8F04N1FQ04EPVBBWGETX5835M8Z9P7WF5X6BHBJW8KKNHZVR1JM9TRVZ9J0G5VDK7BQ9ESFKDCZD4PPB05K0MHW0HAJRKCCJ5PTNFXSRVB4V25D48XT031P0M53WDZRTPEK5G1XQ5KMQHZT06A70GE9N5C7SR4RDVA20AZDBBD1MKPR6NB2FMPAP2DR3NJENPWA", + privateKey = "AZXBY4QCFSH759DWTDCK1Q5D5JRNH3663JP2RTC3TSSTH3ARV0D0", + publicKey = "1C88APBTDQ8A2Q55N1CB3PD91PHYKJHY8QM2CYK0PGRJ0CTK4VPG" + ), + RefreshPlanchetRecord( + blindingKey = "EP0XT6PDDJFKGFVG739QKNZCWRN05497TCBQERFRBJBB7W7Y2JV0", + coinEv = + "J8E5HCMEJZ78BP2VDCNA9FVFBDG9QT4A1FDBSAH6ZZJ2HNJNKYKS57ZVRPZ47B1B8J6WV77PXCDP7EZTX5YTJFPGGAT6W6QWYNGRTW1NPE454J0VPJ3DKT19EA10N27P6MTM35HZZ8GJ8CGW06HGRXSZ5HJA3SQFDRZS2XN68PYM28918ERVV9BDVKFBMRK9NDMWMM79EK198", + privateKey = "3V1SMYW447SGBHHZMZN8KDB21FYGNT0P2WK7A1XAR4AJYMXZNSGG", + publicKey = "SVM02H0CDHXQX2FPR2B31ZZKMT0SQWE2S3VEWSAZDEDVSDDBXW00" + ), + RefreshPlanchetRecord( + blindingKey = "XSW3C40PHH03SX6G74QJ9CJCS6RFA2Q1V79G4ZS0J1F055ETW5TG", + coinEv = + "58FBPPVPMZEPMQB3RV4NJV7GA8GH8XYJ2NZ8N0NYG4M95434E1WT5GSGGM54X5X3FVTKKM5HRG28TCSD5ZCMC1V0KV6YTN1VEGQWVEN92M3TV761KWQDEJEZGHFKE7BMWP75KS37PV9SX4VABSEPS5VZCZB4C3RPG36QH2GH366G0AGQNX2F5T80SBS51Q3XA4FG44KJ14D32", + privateKey = "8SFAB3CA3JQ0KB18MREYVJYJYQ0BNYS1NCVPKHW7WS3YB4QN898G", + publicKey = "4QYR2AQT98J5QNMB44P3ZVFA0WVHJJTWG7WTXRKYJ11X1FYNYDGG" + ), + RefreshPlanchetRecord( + blindingKey = "Y94ST132M2FTD579X8V9E3SK349CZQ5Z70RZ29VSP8S1PM08ZV30", + coinEv = + "HF7CMN9F3Q6EG85C10SQ313336MFQTWJ9ZQ85J4EKWX6Y2546ZNQCPMKDRM2E0KZWQFNR57SKE7EHJ4BWVT14582MT1YG0ZA7RC2DX5VGZSWVT3N568TNSN9TPKYD74KMAH2A0SSXY8QNXF9C3TQD0HPQ409FNV0QR4R65PJ0G4AJAJW3EH6W4CMEXRVXSD8DYT81H6KFPR12", + privateKey = "CTNN080C67WSNA96SY3ECCGYM017T3SYZXTJ3CTDEC23E1ZCXVSG", + publicKey = "R4G1DHWQCB8MS0E5ET8YTC9M9425R6Z146HKM1ADGQJ0Z2QA1YQ0" + ), + RefreshPlanchetRecord( + blindingKey = "2P1C6Z5RJNWPP2YW4SXGED45AM9H4EM7M063V8HKVSRHA4HGC5A0", + coinEv = + "3N07NHNBBSZMQP04EPSN3SC8DZXW0DHWGZ6QD7A20EWD8DDJ00PB2CH11KZP404ZXN752X4TMA6D09C459WXYQDBYQJ70J6920MJ63P9XRMKYHEKWRSJEEC3FX487VFMJJ8ZYYBR3C4YRVPADRXFPZ8T7YJ4667TJKCSRQJGS7V2DS953HQ509M9ZHHWRH19XBFPTET3R71ZT", + privateKey = "DWNV6AD7WKZMMPG342D1KB9PMH6D8RW2ZE5T0MB07KNA88NR2RF0", + publicKey = "21STR11NETSM4VVBZY65NEDNJHXMV27GM1NP2P47DJN4C8AQS21G" + ), + RefreshPlanchetRecord( + blindingKey = "XTRQX3ZCR6MAKY2AJR2KF9C4GJZ0W5HKK71Y0EHG683WBG6VFHQ0", + coinEv = + "867475JHJNY8BWHQH23CTCF3GEZZ2R1JBFKER7FTFQFNA9RCG4JR8Y89165HCW6GZQFBCWN8DQ91V8G3JH9JG4RPNJ6YZ6K1R2JNN4H99AR6DR96SDQ5N8YZSKFBFKD2QE8E9KAD8K05AVXPPQ4R4NDN45R7YTA346S1PB24GR0SCG7TPS1W59596KN3V1F4G17RGAKQA66T0", + privateKey = "T90M7DVZ78WVVDHBGTHW0Y38M88AWM48Q8MEP6WXXK4WGQQBF0C0", + publicKey = "K1MHGCPZSQ8HY8T8WJSAXE5DXH5SMXAGTZ8YZYFWE1ZYBYRHPG6G" + ), + RefreshPlanchetRecord( + blindingKey = "BM2NA21HG2A50GFXZQCQKK5Z23J0Y63SNM966M2FHDPM2XBYJND0", + coinEv = + "GAZ5VA6AD060FE466W5DHJK8E5TBY4MY6TEFD0TZ9BX45CT4S9V5NGV47G26ESW0Q31787GX1QB756J55BNQJY1SASN2942EARZTYCWC9MEA0MAGDFR6S5N6QRNFEACCHE37S4DB2DN4HWKEJP8JM8RADC73KHCBKBVKAEZGYAFD4P99BSER0S9S2ZH7GAWQYZ45YZY7W6C64", + privateKey = "GY4ZPR1BJB5YY0273H46GJTS4HXTWEFPZA4KT5E3Y9H12R8TQCN0", + publicKey = "9ZXVR8Y0TDT2H57M4XZ0HGG1R78RBSN3KZK2WAH678G09XP2WM6G" + ), + RefreshPlanchetRecord( + blindingKey = "X24KD0GD1GC8JWDBB72R94XJMD8FKR7T2GK1MX8DFJN0PY6EKH20", + coinEv = + "5GTW52KR7ACEGQ96SB92KMYBCC8XNPCGV9Q8ZS7MT1N27BH1NYM3BRTR8XJWZZ5FQ5KXEMT3FA374BQTZVPS9GTYHX9J3NMRSBJ6XJA6AGZDMW5VDXS3QWK49271WH1CSHMG2ZJ7XM0NJF86EZN7MPXKTV3WPD25AZ0EA4P980XB7DZ8GA04CVET8K0VFFP4KB13N4088KEB6", + privateKey = "K1W5XB7SESBYTAPP4PCS9DNYY4Z3HQ0464BW4T15VDZ64GFCXBZG", + publicKey = "W88CW2E3Y4Z0VZC8KE9Z4G6H2B8E4BCRGNDPVVD6MF8PNGRHGMH0" + ), + RefreshPlanchetRecord( + blindingKey = "CF2AE3C5HVQJSP0R7CBBTAYQK1NDTG5DTXGW6D6KKWG3053QMHX0", + coinEv = + "FWA6FJY0YM5HWA96QQNH2CGTTD0NR6DTN5YNRDZCBHDTE4A3ZJ5793175J6G0QRPTHNKEMPJFSQEVB4QH7MAHTPSXRJC28WEQ9Q9C7H3R5CDAE9SNMB61RXWV1D1JH195E13690RYZP62KC9349Q25JHADQ4Q4JQJE4MNJK19H3T1WHES285NHCP8BEE3MSE4TH567P1CBK58", + privateKey = "7ANNBTWEQPXP211W3TEHWTNRFG5ZFSCW393W0HG6N24PJN6XK6PG", + publicKey = "2DJ9HFE9HBQ0N8XNEH6BQD7R1H81BJD40Z94FVD016HEHT8JPCRG" + ) + ), + listOf( + RefreshPlanchetRecord( + blindingKey = "J5979NKRJE2PS2NSVS4T4D6D1CSVJF7TBHKXA2FQYEZF6332PV2G", + coinEv = + "8S4CVYXJNHAQ93Z2ENGKJXNGYEXXMKE89KXH1TVRGVEY9PJ1NGFGENCKWZR1ZFC8W0TFXDGPYEAPHVPJAY0PT99G6GNJKDYV0YC89XGZK4F5HGN92BY7HT04AF2HY1RED8B0KKRKX9RV727XSV5QG9TZFBSAP4Y4HXZKEN1DEBRR2WJFYSZ2MYV50XPK5T7TPPMM30TX4PX88", + privateKey = "CJ6QMHX606JYZY9DS5VGPTFKDYYASAD60MFRBZTVEDQQRYX2KX2G", + publicKey = "WYYT5DM2QASGBCTSQKVXT5M7MREXV39MDF11HM5JX4NKMNXWY41G" + ), + RefreshPlanchetRecord( + blindingKey = "BJ9N8QGX1VYYRP1RR4WZGE2NMKY1EG3K6636HPY624MHT1FNK1T0", + coinEv = + "J5XPY46YX3S314FSBJD1MTZXCT87Y9PYKY17TJP5PCJNK2RF5JGMWBJVJQRXZCJCCFPY3FH4R7TD9A3FYB6JF4Y5QNM8Z46M90599H3HP2BSFZM4HW68R5DKPRE11GADR6BAW7ECFYAREDBZJNC5C18BMZWHP5XZ35CZZRWZ4GG1910KXX6A3789KW1028VBC46PHC3M61396", + privateKey = "FE0T9EZM72EZA5PX3TQPVNM9RM5NWJX4M9CEVFXW2BF2HF28AJ7G", + publicKey = "2Z07PKFFWXX53NXPX76HBF5RYRRM6E41A5KD5484VJ2DB7P64AA0" + ), + RefreshPlanchetRecord( + blindingKey = "4B9D3MG4TEHDCETMKBAC3XGYW7A4FFWQ8MEHT7V5SXC0BY0AF3AG", + coinEv = + "CX96G0RTN9N3QFYNKFFWG165XDKYW7FJEMB5K9SKKYB6A9DCK33T34VDDAJBH9VGG6ADXAM2DBVXZYPVKST9VK2KN943A9MRK0HNP8JVEJK84H9CVA4D1Y9N1351V05H2NC8TEDG43CS6RHSD37HX08N3Q03EW887GF2HF97MW23WH9CH2HBBNS9683DH7K5PY6H3MSQZE0S4", + privateKey = "ZS5031P7N3T3B52GNYGF7AEQ44HVCV63Y5Z0Q8E9XME6JC4VGQVG", + publicKey = "QB9HPVS5HXP0TVCACT7YB8B2Z7T9NZD47NA0XR49ZBCKJ03M7NXG" + ), + RefreshPlanchetRecord( + blindingKey = "VTT1W5TK7JTTC6NJZJBQQGD6D005JKXXRGCWRG7AQ6Y86D0JPJHG", + coinEv = + "AGZ8ZKH7HJ2E4QBQVKH0SH94SPZFSQYG1YG0NFSE7T8HB7P6JKPF66ERSFGDH1CYRBJXA0WA1GFPX7WG528NBWXVEAN9HERZZ45XV2F6H4GPTT59F4TK27VK01NRXR93G2NYSS2V3FVVTNNTVB1BE57S5J18J2TD67RMYWWFCG0S1JPXMN196QBKBMB2KQNFP05G9WBPGH4ZT", + privateKey = "MXXTAHN3VWEPD3327H55S8F4JMZHBFFDDV9FBK4SQH8SR0TRRWG0", + publicKey = "XZ4VZMRZMZ32Q3DPPSPFHST8D41MX5YFT6BSF51C0D7M30M16NW0" + ), + RefreshPlanchetRecord( + blindingKey = "047V0KJ23HSGECQRR4MF8DRN3XT3HTHZ0QV585PBJ4HG75CYCPK0", + coinEv = + "DR1K7Y41WADGP405ZZTCPAWBS6QS9WZEW6VX3JGK1KHYNWZKREKA375TXZM73X558449PR805JCECYQNPCQHZ7HG7FNBT8DEDRRNP1JKJX0HPVTD155K1ZNVWWP2YY9THNG8729MKY3Q77CD7NN8MENJYH9E5RA4FXQYYEGQDDPG2YS0TM595HNA2KX1TD7KYYJ86AGT7FX7M", + privateKey = "C5M9VGNMQ2RM92DA92417HA5WGBTP2R2DPZWB6E5MA61S2TNX7W0", + publicKey = "JR43Q0EGAH85B2F1VQDGHZ33318FGB44158TC0ZB4YP8XF1N3KYG" + ), + RefreshPlanchetRecord( + blindingKey = "B4DSQ4BZP3B1QPG21ME2R3BFE0DPX4GRSAVHF3S0V1YEG0JJX620", + coinEv = + "GAH89K67PPPNXZMC8Q7KEA9VHXDV1WB6WHSJXWB0852B4G5FECGAG9W36NV13AYMX7N37Y5X0NZ3WWKJHFKY8R328EEG528KPFV7E7NCJYZZB83PA6DDP5ZVAQF0N79WDANNEXRZEFT1H8CW0ZFK7533TR7FRTCDT9EWRVBDRJ23NX45C6CQVA6CK1JN9P0ZXP69FPJJAYHHE", + privateKey = "A0WS0Z3C9RGJNX9TX5QV4KYYVCTVNMW4N3FD10W97X6AF28KVB10", + publicKey = "B5MBRBBTF6NR53QPFJE5X8WX8GG9Z4TEDG2PCPPVNNB6XHHBNRF0" + ), + RefreshPlanchetRecord( + blindingKey = "ZTQHYH1RPX4XEJV6HG50EQZF6H6M74AY75N3DN4G81ENRS38AM40", + coinEv = + "FKH1C38XNYGYXK0RMMCVJHJAM60JA0M7T0XBVNV0K580M4VEW6SCR528BV65C7J3XYEZHCGMC1822J4AQXT5KS492KNB8G6MCJYJ95FMRD6R47PGWXSBH35JS95WSKE72FSB5W3Y12D6K50D5H0B7SKDQWAK4VZXNHC1S1X01KHV6KVZCTJW763K68ZABAD9R2TQYB686GE8W", + privateKey = "AQZAJRZV4R2FK1ASYEXNJ9E1K3QV0PHSBZNNPZGK2TR2QSBW4XPG", + publicKey = "N53YEANM052FMJZQDJGWGWERVB9X67VDRMA67X31Q7HKXFMJTJVG" + ), + RefreshPlanchetRecord( + blindingKey = "GHJ6XF1XWYY623QDYNDXKTZ0PRSHAN41H8SHA4RGD1HCTV9PV570", + coinEv = + "3ZP6QZ4X6RH1R7C60T51P2HQEYZ4RMFG45MN47J6NQ6YC1M9M2BAFAQBM87JD8T1M69002E5QD2QZDV8HDJ492CT107TWZCE4TQ367GDSTPT6AAFS0JRSDJPBNME5KD6ZH8Q42KNMQ4PCM7GWA2ZQK7V35Z7BDJGWRFF65QGEPBC3F8PKVC92KJ8AN89ANY9H4YM55JFDCRSE", + privateKey = "T7BME1SM7Q6YEKNS6CCEGA7Q54SB1D451H1TJYYNGV6393R2QKGG", + publicKey = "QBTQNQKCGS6CH2W6AQSDSA7M1EFF0DC5Q1AE4MK2BQM0EBG3RJQ0" + ), + RefreshPlanchetRecord( + blindingKey = "1KFVREMW5KJZ3YS65MKCM40GNTCYBREPN7KM26YTVDW0GRPSDYTG", + coinEv = + "K08B7KDNK8C179M042GZ6BYGRNEK2M3YKTR25FZQ1H9AF9EG9GYFEP6Z2KZ3FH1SEPWYWHNQM1N3507PG99B8XWMR1ZG8BR1CFFM2EQFB5W8SVF145NS7SYXSDPMJ7A64NJZEQMZH0VS2XY262M3BDMVWP7FDEG3W1XN4V0S6GXA14GWRNKFG5MYX8RBPFC1ZHPNFHRGD1FT2", + privateKey = "0Q8W768BQYAPSQCD46R3GMH7QAVT6ZJ0T5YA2A3WXXGRGX7X2WGG", + publicKey = "AK1YSV2A9Y61E1XV313KXR1CJY81RZDKZW4TK9CZ82KK7YFJQDY0" + ), + RefreshPlanchetRecord( + blindingKey = "M2FEDXEYNBXQMMWYR36QY9TSVFYW910947RKFBN4AQ3GNZW7565G", + coinEv = + "3R65GCTT7EB685EGVXCJE928KPX1HH8YNHKD3JYYRJ26PD066GG21ZEE5HXSFX9D22NH31KMK4SPKB9QKNAWK539BYSP58ZZ9W011FYTGMKMDB8CMA354JMH03B814Y4MM64EE5BA1BDJ5BDZ0X6CT5BZ51QMD8NBZTKR1SE6YF1YZ6RJDQAG12E312F2VRZNYKXHGXNDSR1J", + privateKey = "V6XNMA56RBR8VRK0GG6Z76STRA6QMNSJZYB15SFS7XX1QPFNN6KG", + publicKey = "KM59YNG5ZZH9RHCN2XF4MY9YEGXC4A7HHN907FGG9C4W6ACFNY20" + ) + ), + listOf( + RefreshPlanchetRecord( + blindingKey = "X84BZ3AZ0KMG3BYVRT3YT04ESCB7R3YEWVBZN5ZDDZ6WN00AEPR0", + coinEv = + "3B5ZHJ3NESVQ31QEBJXAGF6B5SJBEMXX5Q2FDTHRW807FDVQ3Y7T7KVQ5JSMB1DA3EQCQDNJYVZDQQQDHHHDR5TDEZZDJHX9S94KEKTRMNJDPFXSYABCCMWR7R4H1R7W6TM5Q4B5H2FYZXAVCAJJMC13ZK53N83SRRF8EVAJ2ZB51D6X7WPWQ1MQX3DPTXVZV4EGGPMYDP720", + privateKey = "FFZ1E4KGF8Z6HHYGQC1QR6RJFDQ3QRPCDM58NMK6YTFTSK6GTA9G", + publicKey = "Y7F33NG04MEFAK0QCBAHG325705B1326HCJ9WFXQCZVKMJJ8ZE30" + ), + RefreshPlanchetRecord( + blindingKey = "E3QTYEC6NNZBQ6QTSX7TJ50CQGM3QCBABVPDSZANHFJNX8CW2Q90", + coinEv = + "6NE9SY9SZXTCJP4SW01T5R3YYZFCEG7AR5VJ4JNVQ9WPB7A1DGVXDZQ68233KSG5GEBV4W4T74CK5AC2MDKST7RCJVFSVP1B4EETSDVNWSJ8VF2V7BY8W9S85Z06WJE223PW692CEE4JA0Y83Z40Z69P28NS430BX9VMWRXMZMCH9FKB92TCVZGTXM3CJ2ZG5RRPNZB60VKKT", + privateKey = "ABAF6S5WZMZZ1ZWXJG7JVEQTMJB5ZBRR4T554Q0SNDZDRFG9MNMG", + publicKey = "HFVN1JANX4A4DSB4K458255GC5S879QTSBF2DDEM2K0HPZQPHF0G" + ), + RefreshPlanchetRecord( + blindingKey = "VRWF9B40EHKB4NZ6FXQG1P26XESD7FZXSEA9X53DZ1WVXS22GZAG", + coinEv = + "EWSVHPMDEBPW256A7K20TSW2T9EBQNKCHQWRJ1HJ85NW9K058H9C5C8QZ1R2ZEKDRYSEMPM920HXSE863BG8VKJHDR1ADQFREWH6TA2QA2ZANSMNJDHCA1EMG78A9PSMZCYE8Y82SY70MJSKZG9ZF0F5EG3KJ14X21RC6HY41P79NVETG4R11GF7MADSTF11G5CNB8EX26RN8", + privateKey = "DKHBJ9SRVEGAD7Q6K7PMH8DMA8WSRCK31PEAD6NWA4T6QRCMBW60", + publicKey = "DMVWT6WPYWHHX4G0H124HDQ6QDA1T447WQQW9NVR1ABJZ31E3YA0" + ), + RefreshPlanchetRecord( + blindingKey = "7HJN1EJK12M3JF7H6J6A82X9VB0VWCAC5MW4DA0W7524ZA620F3G", + coinEv = + "5YFPXNJ0SKNBJ6ATMD4JAPCMDFQD25RWYYQZXM7X6W8V9VBVBRT4M94MTMSE6W5H56XMCT5MF51MJVM9W090223R5VWTDQ9KEAHT45VPASHXNJ817R1A2S50F7TZTDHKE3ZKPJ1ZSTC9XDS57N0JGZ4AK3606PZ5211G8BDWSAGY8N0JYNASZRZ9JABN4HNTKRXRYZKN0V240", + privateKey = "AXZTP48HT58DB4BFKREZ34ARBVREC1BECK0KEYBWFWVP7K17ZQG0", + publicKey = "HP8PY0NPT0NV32RNMEYZ63YSDYB2T9J64KXAYV76M1N9Z4GPF8YG" + ), + RefreshPlanchetRecord( + blindingKey = "KK0VT0E3KMHG39VT0B5NNTEXC9HMT9HJ6554THEVWHPHS96H4FS0", + coinEv = + "EMNAXG036TG4JNZ2TWBE688THQ1BZW8H5PCYMD4Z71K5FHSB6N8QE54V03RK0GJQVCDYPH699PPR5HDSWCBV13YG6WX9DX097VPMKTHSD7CYGH611RGDJ3KJXJQXQCDYXC869JZ9ZTBNGYGVZDVP16141Q32ARRF59T0MDKR6C4BJQ1V32Y2ZWHRD5QVRBAYS90KPDW6G00D6", + privateKey = "4NFWW24DP7V17ZN4D12NW6TENEMF54XZMNB4XVXM1Q31AMC8DKHG", + publicKey = "A7F9SY8413B3EQSVCES7AFDYRJH768J8AA8DVDR4BR06SHXT0W1G" + ), + RefreshPlanchetRecord( + blindingKey = "1KB9TCM2GDT61ARG99NA5TCG35H3P4K6ZGXEQA33P3XNQPSX33G0", + coinEv = + "1MRTJBESYQ1MSJC117JHRT90GS7WPCF5ACA198RGV4X0E464W2KMPFS4K43Z94NJ8QKP40YZCRX22ZR23MMCFWGNS5KYDV57JH4VZM6YSDVCRDGCBQDB87BN9TS2PXK8GXK3JA3G84X5HBGY7CX870B3MWBHW0V6FHMQNDXVFP1PCYZT0T0GAPPQ02MZ6Q8JGMSXT0QFXF9P4", + privateKey = "289K9AW8M3N45RZZNGEYBFHZEGCTKRY9TB15F5SNVAQ09BHKHEWG", + publicKey = "BZFET2N4MFYPQSMDASD7DJHA0EC8KGSB8RS3G34G8B5D6S9W1B90" + ), + RefreshPlanchetRecord( + blindingKey = "B28XC87F006EE1B0GRMBEW3P88J324C7BYPS3SCVHAB3KRA7JBM0", + coinEv = + "8S771KDCPD9XFRXDT7938T8HX2XNGM7FH3WJGTYBD343GY5E427ZPD0QA4XAJYWN9038K0BH6Y0R2G42T8PCAM4W0YP31FPCABPZXPCXZP52ZDB6QR3ZG01KS9GBWAJ4PPKJG24MREBVTX37R90WA44G7Y8G5FCDBEQR1TZ3PZ0Q4C3K0Z3QP1HNNEH3WEP2QPFJD82YFF1G2", + privateKey = "592P3HWA2D245M1ZHA4AMSRK6Y5A9YFVP70VAKBAF6GSVVZE7MW0", + publicKey = "VS7N76T06X9GPEPGF1K7CD3ZTCJ4R8DJW715NRE30HT92JXQ076G" + ), + RefreshPlanchetRecord( + blindingKey = "9VQATS7SQ9QRM19602PN3QF16JEF9FM8GT1BZC8WRM9Y6HRV9HMG", + coinEv = + "CMZK656TCB6BRDRXVZ625SYZTGVKX56J77NE9B6T0EHRM8DRD7MR5WDRWQV8Z5WWDTAGKPCPA3XHCTGW5T156KK9RM340N53VPYJTDXH22CKY68PY8J4J40XG40YZW3F6TREE5TRFF51VJT7QV8CHW9PM67ZK2ZZWGVXV3KJF2HVDYFS2X8BHC19B266H5BK5EX6NJ65E9ADG", + privateKey = "4QVMP2V1FJQKTCTVXFBDCCR29T3BG40RCFZCJDPG6WPAS89EWWF0", + publicKey = "MV5RPV33Z2EFXSA6XPV0GVP2WBAP580JXAB2WD0G86FM29VM2X9G" + ), + RefreshPlanchetRecord( + blindingKey = "ZTKWFCA58WC7GPYDYBMAXCH12KPTRGTXR2WMYSX8HPSAZ4M47480", + coinEv = + "7Z9T151XYF58P4T8B7M3DS8VYFS7MXQPYG40V1JZ0SXXFNVQ1E6GX6C64F36FQADNPDB7S4YDNR80TGCACPXD5ZEP7JQ514Z1ZS43D739BBGPWH5QHGGDN91V2KDSDD8AS4V2M56CF26G0RBVKV8EZTM6ATQCQBVJE6Y4S98VP31WECYSW8RAYKHMHWKN5DHF1NMZMY86KA5E", + privateKey = "M87WJ5ECSYVVGK0PDTWPJCQS5ZE27D79RAZVGNDB8HWGC4R0X1G0", + publicKey = "7ANMTX5E74K8074Z50E36D8QX5J9TQ6B7WPN7XK3DPV4YFS1DX7G" + ), + RefreshPlanchetRecord( + blindingKey = "2522XN7JAFCVXSFMG031CSA20DFMY2MF5ZTZT6W3HP59RPW5S34G", + coinEv = + "5R0DF70BTV34K5X8DQG8Q1XY55RXCRQ8KF2WK12QP90DBY2QRWF5CJDE20X5BFWD5YKWVMKHV8QWE6SFGSHDBSJADYKA7WZXGBJ2EJPAYXEHMTTJ89KH60KZME1SDBBYZA239M98JV56ZAMZPNMG928Y4P27F5BPBSGQK3E3A700BDNW1TTJ5VYPXN8SNJANW74YFPJZN2HWY", + privateKey = "ZAX0KQPGATJT5EMWP7D6G4TB8GH6ZS6C17PJ91FS4F4VWX16H26G", + publicKey = "DNYGRGC1R41AH28FY063N71RDYPFYMDB20N491R8297R33G72FE0" + ) + ) + ), + transferPrivateKeys = listOf( + "KXR71TDT7A5GBZ6D92PWYC7PY9Q18KMVC4F4VRNYZ4NQ2V6DDY50", + "DWRWHG4NN81SWFYVJ1B1EKN9RSXRBJYV06Z8SQWWWTN6W6TRPCEG", + "3ZSZTTF3S1BG4MDW1GF8PE5EX2A2WD6WBTR8SYSD6GZBS4D4Z780" + ), + transferPublicKeys = listOf( + "2KQHHDF90PETJ852EX67W441PGZ5M47ZRD8XT0DY0HGY5Y1XWXSG", + "ANNVKXH143SB5EXDMS9XN2NXV0ETVA7KFJPS94Y6356H16K0JH5G", + "JB60PMEWBJ0Z51QGBHNHVQ51V8FT0C97QX64DFSK8SXEQNSYV1EG" + ), + amountRefreshOutput = Amount("TESTKUDOS", value = 1, fraction = 81000000), + amountRefreshInput = Amount("TESTKUDOS", value = 1, fraction = 95000000), + timestampCreated = Timestamp(1593697014952), + finishedTimestamp = null + ), + kappaKeys = listOf( + EcdheKeyPair( + publicKey = Base32Crockford.decode("2KQHHDF90PETJ852EX67W441PGZ5M47ZRD8XT0DY0HGY5Y1XWXSG"), + privateKey = Base32Crockford.decode("KXR71TDT7A5GBZ6D92PWYC7PY9Q18KMVC4F4VRNYZ4NQ2V6DDY50") + ), + EcdheKeyPair( + publicKey = Base32Crockford.decode("ANNVKXH143SB5EXDMS9XN2NXV0ETVA7KFJPS94Y6356H16K0JH5G"), + privateKey = Base32Crockford.decode("DWRWHG4NN81SWFYVJ1B1EKN9RSXRBJYV06Z8SQWWWTN6W6TRPCEG") + ), + EcdheKeyPair( + publicKey = Base32Crockford.decode("JB60PMEWBJ0Z51QGBHNHVQ51V8FT0C97QX64DFSK8SXEQNSYV1EG"), + privateKey = Base32Crockford.decode("3ZSZTTF3S1BG4MDW1GF8PE5EX2A2WD6WBTR8SYSD6GZBS4D4Z780") + ) + ) + ) + ) + for (v in vectors) testRefreshVector(v) + } + + private fun testRefreshVector(v: RefreshVector) { + val record = + refresh.createRefreshSession("example.org", v.meltCoin, v.meltFee, v.newCoinDenominations, v.kappa) { + v.kappaKeys[it] + } + // use expected timestamp, so we don't need to mock system clock + assertTrue(v.refreshSessionRecord.timestampCreated.ms < record.timestampCreated.ms) + assertEquals(v.refreshSessionRecord, record.copy(timestampCreated = v.refreshSessionRecord.timestampCreated)) + } + + private class SignCoinLinkVector( + val oldCoinPrivateKey: String, + val newDenomHash: String, + val oldCoinPublicKey: String, + val transferPublicKey: String, + val coinEv: String, + val signature: String + ) + + @Test + fun testSignCoinLink() { + val vectors = listOf( + SignCoinLinkVector( + oldCoinPrivateKey = "884XH9DBQT9MA34YKCR7WPEP2HH3J1R9C3G7MSB8C3NHB0XEVZW0", + newDenomHash = "ZVZMM6GEFETH1S71BRKFKB03BTSTA8JZH26Q1PZED78NZ50EVVT0DMDBPDN5EBY90SK4AWT0J8CNPD7NB2TSCFMSKAS5V6DK26GEKT0", + oldCoinPublicKey = "3GSHK9JCTMEAZY7BMC1QQD28ABEJEJJJXHPVF2YXTET97WB81GNG", + transferPublicKey = "ANNVKXH143SB5EXDMS9XN2NXV0ETVA7KFJPS94Y6356H16K0JH5G", + coinEv = "8S4CVYXJNHAQ93Z2ENGKJXNGYEXXMKE89KXH1TVRGVEY9PJ1NGFGENCKWZR1ZFC8W0TFXDGPYEAPHVPJAY0PT99G6GNJKDYV0YC89XGZK4F5HGN92BY7HT04AF2HY1RED8B0KKRKX9RV727XSV5QG9TZFBSAP4Y4HXZKEN1DEBRR2WJFYSZ2MYV50XPK5T7TPPMM30TX4PX88", + signature = "H1V8D30JTVW8F7KERCWRDHPVVV6BZXNE0QG4V7ANEPTVFT2NKEWCCRM8NSG6D6MBA80ZSSYG1KYZXR8CWG0HV6N7QA48C4YEWK4M030" + ), + SignCoinLinkVector( + oldCoinPrivateKey = "884XH9DBQT9MA34YKCR7WPEP2HH3J1R9C3G7MSB8C3NHB0XEVZW0", + newDenomHash = "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R", + oldCoinPublicKey = "3GSHK9JCTMEAZY7BMC1QQD28ABEJEJJJXHPVF2YXTET97WB81GNG", + transferPublicKey = "ANNVKXH143SB5EXDMS9XN2NXV0ETVA7KFJPS94Y6356H16K0JH5G", + coinEv = "CX96G0RTN9N3QFYNKFFWG165XDKYW7FJEMB5K9SKKYB6A9DCK33T34VDDAJBH9VGG6ADXAM2DBVXZYPVKST9VK2KN943A9MRK0HNP8JVEJK84H9CVA4D1Y9N1351V05H2NC8TEDG43CS6RHSD37HX08N3Q03EW887GF2HF97MW23WH9CH2HBBNS9683DH7K5PY6H3MSQZE0S4", + signature = "90NEPFDDK99524R9RZ3H4NEK6S5N71CQN6DAXJ9S87YASHB0JYG3VJKSV7BVJVCRCHWDYYS61CXQ4XWW9DMS6HAV0PAPWEY7FPKB430" + ), + SignCoinLinkVector( + oldCoinPrivateKey = "8G7F1X5EQV9P7JK1YVY5MFYX18Z85FRQFCKFQ9J5HX8B03QEAR80", + newDenomHash = "HDYHXFJ65YSW7BEVCP1G282E4QHQ9KSF8KBQNM4JN4T3KD30YVN2G6G1HFWEMSHJGAW1VM6N8HDEYRXVYJ3A16C58WM6QATMENGJ5CR", + oldCoinPublicKey = "4670V0PA3JG928CJFQHGJAC3DKCPJR3HMRRYHKAHTEEGQDK78HAG", + transferPublicKey = "BH4PSJF3T0S9QNFARDXTBWXWT46AX5CH9JENP5D1HQVTNK9YXN7G", + coinEv = "PDGXW6PDZTFEGMYFCVW972V4WR9WXTYW6JBDCW52DCEPCECBK5AX8R276Q5JZ38ABQ0X0E8NSSV28H56CX3MR3YTXFAJKHY9M93EECWDCK6AA4259M6DYSS1D4AMQ2PQJH3Q0BFFXDS9X5Z6VKFM0EH05KM91SBB6679NC2SF0B4A1G2N5KTQEXCQADNJKHGGZJ704PBPYCGR", + signature = "7STWC1GZ6WWYBYWBNA1BDKFYAX8TCRGFP36A3W0TH72K51R9DXMVRA2H6N21CSA40Z3SQJ0NCFBNTR0781PX3027QX7KZ9KC7Q3SW20" + ), + SignCoinLinkVector( + oldCoinPrivateKey = "8G7F1X5EQV9P7JK1YVY5MFYX18Z85FRQFCKFQ9J5HX8B03QEAR80", + newDenomHash = "Y7N6C15W0ADR06Q2QKK15VWKV09FMHC0BHT0ZZ95FP8T7G7T2GCYWVMYRE605QCP7NQ02QYH3Y9V834G37K8KT7V3AY75N715AYX170", + oldCoinPublicKey = "4670V0PA3JG928CJFQHGJAC3DKCPJR3HMRRYHKAHTEEGQDK78HAG", + transferPublicKey = "BH4PSJF3T0S9QNFARDXTBWXWT46AX5CH9JENP5D1HQVTNK9YXN7G", + coinEv = "63X0T7DWHJHMKR7GX4D9CTNHVMGRKBN4C2VHBSNY6W8JMJYJNDD40XZXG9RB7V4W7HHJ605XK6R9VSBMJ8WCF5AQ3A0631F6MFTSEKTKJDMMP8006DGZ0XMW43JD0PD2XXX42HPAD6DY0ST941NYKNMA050ZMTV1JKNYW9495RVXQCMKPJ759KCC4BA6066E17Q7AVNS6EJD8", + signature = "5NVY7F9ZBSHY0JP1Q8JFTJYCY63QRT2JNV3R25050451ACYDXZ4YT0CE3TQYAECMVZ3F092GN7TQFNJF3G24JZN6T7XHDDG4MT0M82R" + ) + ) + for (v in vectors) testSignCoinLinkVector(v) + } + + private fun testSignCoinLinkVector(v: SignCoinLinkVector) { + val signature = + refresh.signCoinLink(v.oldCoinPrivateKey, v.newDenomHash, v.oldCoinPublicKey, v.transferPublicKey, v.coinEv) + assertEquals(v.signature, signature) + } + +} diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt b/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt index fbd6c65..c0efbac 100644 --- a/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt +++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt @@ -1,5 +1,6 @@ package net.taler.wallet.kotlin +import com.soywiz.klock.DateTime import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray @@ -10,6 +11,7 @@ class Timestamp( companion object { const val NEVER: Long = -1 + fun now(): Timestamp = Timestamp(DateTime.now().unixMillisLong) } /** diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt new file mode 100644 index 0000000..4ffb3b9 --- /dev/null +++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt @@ -0,0 +1,256 @@ +package net.taler.wallet.kotlin.crypto + +import net.taler.wallet.kotlin.Amount +import net.taler.wallet.kotlin.Base32Crockford +import net.taler.wallet.kotlin.CoinRecord +import net.taler.wallet.kotlin.DenominationRecord +import net.taler.wallet.kotlin.Timestamp +import net.taler.wallet.kotlin.crypto.Signature.Companion.WALLET_COIN_LINK +import net.taler.wallet.kotlin.crypto.Signature.Companion.WALLET_COIN_MELT +import net.taler.wallet.kotlin.crypto.Signature.PurposeBuilder + +internal class Refresh(private val crypto: Crypto) { + + data class RefreshSessionRecord( + + /** + * Public key that's being melted in this session. + */ + val meltCoinPub: String, + + /** + * How much of the coin's value is melted away with this refresh session? + */ + val amountRefreshInput: Amount, + + /** + * Sum of the value of denominations we want to withdraw in this session, without fees. + */ + val amountRefreshOutput: Amount, + + /** + * Signature to confirm the melting. + */ + val confirmSig: String, + + /** + * Hashed denominations of the newly requested coins. + */ + val newDenominationHashes: List<String>, + + /** + * Denominations of the newly requested coins. + */ + val newDenominations: List<String>, + + /** + * Planchets for each cut-and-choose instance. + */ + val planchetsForGammas: List<List<RefreshPlanchetRecord>>, + + /** + * The transfer public keys, kappa of them. + */ + val transferPublicKeys: List<String>, + + /** + * Private keys for the transfer public keys. + */ + val transferPrivateKeys: List<String>, + + /** + * The no-reveal-index after we've done the melting. + */ + val noRevealIndex: Int?, + + /** + * Hash of the session. + */ + val hash: String, + + /** + * Timestamp when the refresh session finished. + */ + val finishedTimestamp: Timestamp?, + + /** + * When was this refresh session created? + */ + val timestampCreated: Timestamp, + + /** + * Base URL for the exchange we're doing the refresh with. + */ + val exchangeBaseUrl: String + ) + + data class RefreshPlanchetRecord( + /** + * Public key for the coin. + */ + val publicKey: String, + /** + * Private key for the coin. + */ + val privateKey: String, + /** + * Blinded public key. + */ + val coinEv: String, + /** + * Blinding key used. + */ + val blindingKey: String + ) + + data class DenominationSelectionInfo( + val totalCoinValue: Amount, + val totalWithdrawCost: Amount, + val selectedDenominations: List<SelectedDenomination> + ) + + data class SelectedDenomination(val count: Int, val denominationRecord: DenominationRecord) + + /** + * Create a new refresh session. + */ + fun createRefreshSession( + exchangeBaseUrl: String, + meltCoin: CoinRecord, + meltFee: Amount, + newCoinDenominations: DenominationSelectionInfo, + kappa: Int = newCoinDenominations.selectedDenominations.size + ) : RefreshSessionRecord { + return createRefreshSession(exchangeBaseUrl, meltCoin, meltFee, newCoinDenominations, kappa) { + crypto.createEcdheKeyPair() + } + } + + /** + * Create a new refresh session and allow to provide transfer key pairs for testing. + */ + fun createRefreshSession( + exchangeBaseUrl: String, + meltCoin: CoinRecord, + meltFee: Amount, + newCoinDenominations: DenominationSelectionInfo, + kappa: Int = newCoinDenominations.selectedDenominations.size, + kappaKeys: (Int) -> EcdheKeyPair + ): RefreshSessionRecord { + val sessionHashState = crypto.getHashSha512State() + + // create fresh transfer keys, one pair for each selected denomination (kappa-many) + val transferPublicKeys = ArrayList<String>() + val transferPrivateKeys = ArrayList<String>() + for (i in 0 until kappa) { + val transferKeyPair = kappaKeys(i) + sessionHashState.update(transferKeyPair.publicKey) + transferPrivateKeys.add(Base32Crockford.encode(transferKeyPair.privateKey)) + transferPublicKeys.add(Base32Crockford.encode(transferKeyPair.publicKey)) + } + + // add denomination public keys to session hash + val newDenominations = ArrayList<String>() + val newDenominationHashes = ArrayList<String>() + for (selectedDenomination in newCoinDenominations.selectedDenominations) { + for (i in 0 until selectedDenomination.count) { + newDenominations.add(selectedDenomination.denominationRecord.denomPub) + newDenominationHashes.add(selectedDenomination.denominationRecord.denomPubHash) + sessionHashState.update(Base32Crockford.decode(selectedDenomination.denominationRecord.denomPub)) + } + } + + // add public key of melted coin to session hash + sessionHashState.update(Base32Crockford.decode(meltCoin.coinPub)) + + // calculate total value with all fees and add to session hash + val (totalOutput, withdrawFee) = calculateOutputAndWithdrawFee(newCoinDenominations.selectedDenominations) + val valueWithFee = totalOutput + withdrawFee + meltFee + sessionHashState.update(valueWithFee.toByteArray()) + + val planchetsForGammas = ArrayList<ArrayList<RefreshPlanchetRecord>>() + for (i in 0 until kappa) { + val planchets = ArrayList<RefreshPlanchetRecord>() + for (selectedDenomination in newCoinDenominations.selectedDenominations) { + for (k in 0 until selectedDenomination.count) { + val coinNumber = planchets.size + val transferPrivateKey = Base32Crockford.decode(transferPrivateKeys[i]) + val oldCoinPub = Base32Crockford.decode(meltCoin.coinPub) + val transferSecret = crypto.keyExchangeEcdheEddsa(transferPrivateKey, oldCoinPub) + val fresh = crypto.setupRefreshPlanchet(transferSecret, coinNumber) + val publicKeyHash = crypto.sha512(fresh.coinPublicKey) + val denominationPub = Base32Crockford.decode(selectedDenomination.denominationRecord.denomPub) + val ev = crypto.rsaBlind(publicKeyHash, fresh.bks, denominationPub) + val planchet = RefreshPlanchetRecord( + blindingKey = Base32Crockford.encode(fresh.bks), + coinEv = Base32Crockford.encode(ev), + privateKey = Base32Crockford.encode(fresh.coinPrivateKey), + publicKey = Base32Crockford.encode(fresh.coinPublicKey) + ) + planchets.add(planchet) + sessionHashState.update(ev) + } + } + planchetsForGammas.add(planchets) + } + + val sessionHash = sessionHashState.final() + + // make a signature over sessionHash, value (again?), meltFee and meltCoin public key with meltCoin private key + val confirmData = PurposeBuilder(WALLET_COIN_MELT) + .put(sessionHash) + .put(valueWithFee.toByteArray()) + .put(meltFee.toByteArray()) + .put(Base32Crockford.decode(meltCoin.coinPub)) + .build() + val confirmSignature = crypto.eddsaSign(confirmData, Base32Crockford.decode(meltCoin.coinPriv)) + + return RefreshSessionRecord( + confirmSig = Base32Crockford.encode(confirmSignature), + exchangeBaseUrl = exchangeBaseUrl, + hash = Base32Crockford.encode(sessionHash), + meltCoinPub = meltCoin.coinPub, + newDenominationHashes = newDenominationHashes, + newDenominations = newDenominations, + noRevealIndex = null, + planchetsForGammas = planchetsForGammas, + transferPrivateKeys = transferPrivateKeys, + transferPublicKeys = transferPublicKeys, + amountRefreshOutput = totalOutput, + amountRefreshInput = valueWithFee, + timestampCreated = Timestamp.now(), + finishedTimestamp = null + ) + } + + private fun calculateOutputAndWithdrawFee(selectedDenomination: List<SelectedDenomination>): Pair<Amount, Amount> { + val currency = selectedDenomination[0].denominationRecord.value.currency + var total = Amount.zero(currency) + var fee = Amount.zero(currency) + for (ncd in selectedDenomination) { + total += ncd.denominationRecord.value * ncd.count + fee += ncd.denominationRecord.feeWithdraw * ncd.count + } + return Pair(total, fee) + } + + fun signCoinLink( + oldCoinPrivateKey: String, + newDenominationHash: String, + oldCoinPublicKey: String, + transferPublicKey: String, + coinEv: String + ): String { + val coinEvHash = crypto.sha512(Base32Crockford.decode(coinEv)) + val coinLink = PurposeBuilder(WALLET_COIN_LINK) + .put(Base32Crockford.decode(newDenominationHash)) + .put(Base32Crockford.decode(oldCoinPublicKey)) + .put(Base32Crockford.decode(transferPublicKey)) + .put(coinEvHash) + .build() + val coinPrivateKey = Base32Crockford.decode(oldCoinPrivateKey) + val sig = crypto.eddsaSign(coinLink, coinPrivateKey) + return Base32Crockford.encode(sig) + } + +} |