gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-kotlin] branch master updated (ba115f8 -> 86bd043)


From: gnunet
Subject: [taler-wallet-kotlin] branch master updated (ba115f8 -> 86bd043)
Date: Wed, 24 Jun 2020 22:53:56 +0200

This is an automated email from the git hooks/post-receive script.

torsten-grote pushed a change to branch master
in repository wallet-kotlin.

    from ba115f8  Add test for setting up refresh planchets
     new 7f721d0  Add Amount class with tests
     new f86015e  Add Timestamp class with tests
     new f6d37f2  Add SignaturePurposeBuilder and tests
     new 86bd043  Add Planchet creation with tests and platform specific secure 
random bytes

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .idea/dictionaries/user.xml                        |   1 +
 .../taler/wallet/kotlin/crypto/CryptoFactory.kt    |   4 +
 .../net/taler/wallet/kotlin/crypto/PlanchetTest.kt | 125 ++++++++++
 .../kotlin/net/taler/wallet/kotlin/Amount.kt       | 194 +++++++++++++++
 .../kotlin/net/taler/wallet/kotlin/Timestamp.kt    |  27 +++
 .../net/taler/wallet/kotlin/crypto/Crypto.kt       |   1 +
 .../net/taler/wallet/kotlin/crypto/CryptoImpl.kt   |  30 ++-
 .../net/taler/wallet/kotlin/crypto/Planchet.kt     |  68 ++++++
 .../net/taler/wallet/kotlin/crypto/Signature.kt    |  46 ++++
 .../kotlin/net/taler/wallet/kotlin/AmountTest.kt   | 270 +++++++++++++++++++++
 .../net/taler/wallet/kotlin/TimestampTest.kt       |  75 ++++++
 .../taler/wallet/kotlin/crypto/SignatureTest.kt    | 184 ++++++++++++++
 .../taler/wallet/kotlin/crypto/CryptoFactory.kt    |   4 +
 .../taler/wallet/kotlin/crypto/CryptoFactory.kt    |   6 +
 14 files changed, 1026 insertions(+), 9 deletions(-)
 create mode 100644 
src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/PlanchetTest.kt
 create mode 100644 src/commonMain/kotlin/net/taler/wallet/kotlin/Amount.kt
 create mode 100644 src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt
 create mode 100644 
src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt
 create mode 100644 
src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt
 create mode 100644 src/commonTest/kotlin/net/taler/wallet/kotlin/AmountTest.kt
 create mode 100644 
src/commonTest/kotlin/net/taler/wallet/kotlin/TimestampTest.kt
 create mode 100644 
src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt

diff --git a/.idea/dictionaries/user.xml b/.idea/dictionaries/user.xml
index 02282e7..c5ce0d6 100644
--- a/.idea/dictionaries/user.xml
+++ b/.idea/dictionaries/user.xml
@@ -7,6 +7,7 @@
       <w>nacl</w>
       <w>planchet</w>
       <w>planchets</w>
+      <w>taler</w>
     </words>
   </dictionary>
 </component>
\ No newline at end of file
diff --git 
a/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt 
b/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
index cf754f2..7435c3f 100644
--- a/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
+++ b/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
@@ -27,6 +27,10 @@ internal object CryptoJvmImpl : CryptoImpl() {
         return output
     }
 
+    override fun getRandomBytes(num: Int): ByteArray {
+        return sodium.randomBytesBuf(num)
+    }
+
     override fun eddsaGetPublic(eddsaPrivateKey: ByteArray): ByteArray {
         return sodium.cryptoSignSeedKeypair(eddsaPrivateKey).publicKey.asBytes
     }
diff --git 
a/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/PlanchetTest.kt 
b/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/PlanchetTest.kt
new file mode 100644
index 0000000..d7f1dae
--- /dev/null
+++ b/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/PlanchetTest.kt
@@ -0,0 +1,125 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.crypto.Planchet.CreationRequest
+import net.taler.wallet.kotlin.crypto.Planchet.CreationResult
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+
+// TODO move to commonTest once RsaBlinding is implemented everywhere
+class PlanchetTest {
+
+    private val crypto = CryptoFactory.getCrypto()
+    private val planchet = Planchet(crypto)
+
+    private class PlanchetVector(val request: CreationRequest, val 
eddsaKeyPair: EddsaKeyPair, val blindingFactor: String, val result: 
CreationResult)
+
+    @Test
+    fun testCreate() {
+        val vectors = listOf(
+            PlanchetVector(
+                CreationRequest(
+                    denomPub = 
"040000XVGVWCHVQVTQ06Q5V0XRAVQKPPZQZ68GYVXSC5RAG37VDCG0CEQHS4876BX6DDABB2WFY7TRJ7MFKTMMDF7A7ZW9PKQ8S3RQ15TVTKWBFGGKBKYSP6CVHNG9AY738NCPC8AFWYGP8J2VJE9HRR7M1GQK19E2M7Q2Y54KCSZ583BTNX275DW6EYYE1KBV4FK009Z621EHF5R87S6VQDSBCKSK15JCH1JYC2VPRHHAEGRA2WYX1HD9KFET0C9G1CZJB1MHZ5Z7Y803YZJH441P3PJJTRB9WCTA03H6M43CJ9MB33BEJ3KR22R8CS0D6QC2E7ZQS5MGBWCF51FK97SHCJW93SAT7VHB3YX5VVDNTW9N3SDW56HNWT11D306H9VN7BTP84T404VF482Y09K4SHEF5704002",
+                    feeWithdraw = 
Amount.fromJSONString("KOADwTb3:9329564218.42023"),
+                    reservePriv = 
"9TM70AKDTS57AWY9JK2J4TMBTMW6K62WHHGZWYDG0VM5ABPZKD40",
+                    reservePub = 
"8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0",
+                    value = 
Amount.fromJSONString("KOADwTb3:3932974019564218.48282023")
+                ),
+                EddsaKeyPair(
+                    
Base32Crockford.decode("GX5DGW3RJ4HMXS53W29TK2667NWA3Z4WB41X7GPAX3WX4036VQGG"),
+                    
Base32Crockford.decode("Z6DXSXGEQ8C4G50FF6TG6ZKBH11APJ2HSNS21BAR72VY3KV7CC90")
+                ),
+                "H58V73J73HXTA9CPT8ZZ5G7VVKCWZFAE1TMCCV176QBQTPBB2H40",
+                CreationResult(
+                    blindingKey = 
"H58V73J73HXTA9CPT8ZZ5G7VVKCWZFAE1TMCCV176QBQTPBB2H40",
+                    coinEv = 
"AK23AD09K8462T621RPER66WQRNE845JAMBT4Y1AA39M22M5K0DZFPH2P5V9E8RD0VC1Z915WB432D2C1BHKHZGP62X9A424JZRPVTFJGYCFYHH52BG0VTJ58ZK70S52KYC2DW4Z0XHKBW0BW3F8NAGGTTGE6NF6EJ3SXYBVYTN0TDJE7HED3ZEGM34N73656TADK0VNZN04BQQNZYW7WWDGT5A06CZTCS4HTSD74CVNJ70CQQQ1C9D14AA75NJ902K6FC7ANBHFGENZXAYNAC0WQQQ6J7XW0TCC3N39TYSCC7TJVH7FZQEXKE8RDGT873QX4C7XDVV6TNWEBBCPM9AABW9PXBEQSM55DT79GVZ7156MZWJKGZAGNVX1FASAY0J3CW507672300R603MW1RRRC",
+                    coinPriv = 
"GX5DGW3RJ4HMXS53W29TK2667NWA3Z4WB41X7GPAX3WX4036VQGG",
+                    coinPub = 
"Z6DXSXGEQ8C4G50FF6TG6ZKBH11APJ2HSNS21BAR72VY3KV7CC90",
+                    coinValue = 
Amount.fromJSONString("KOADwTb3:3932974019564218.48282023"),
+                    denomPub = 
"040000XVGVWCHVQVTQ06Q5V0XRAVQKPPZQZ68GYVXSC5RAG37VDCG0CEQHS4876BX6DDABB2WFY7TRJ7MFKTMMDF7A7ZW9PKQ8S3RQ15TVTKWBFGGKBKYSP6CVHNG9AY738NCPC8AFWYGP8J2VJE9HRR7M1GQK19E2M7Q2Y54KCSZ583BTNX275DW6EYYE1KBV4FK009Z621EHF5R87S6VQDSBCKSK15JCH1JYC2VPRHHAEGRA2WYX1HD9KFET0C9G1CZJB1MHZ5Z7Y803YZJH441P3PJJTRB9WCTA03H6M43CJ9MB33BEJ3KR22R8CS0D6QC2E7ZQS5MGBWCF51FK97SHCJW93SAT7VHB3YX5VVDNTW9N3SDW56HNWT11D306H9VN7BTP84T404VF482Y09K4SHEF5704002",
+                    denomPubHash = 
"XB6T8NRGSRPWBM2YGS3R0AQYGEMK7PAM3CQRX6XM04B4N48PWRVZ5DG5JTT0NNQAGHN5HTGSCPR06R6B5NJBZ2DT5VZSQRD8FTNFPEG",
+                    reservePub = 
"8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0",
+                    withdrawSig = 
"SNTZ4DWRVJBK89YGAZ60EDV0T7BM80MD6J6P88BRKDQFP331CXPSGM45CMCVBB7GR6X2FWQC5EJGR0J8KBR459PSGT18DA5PMQZKG08",
+                    coinEvHash = 
"FW36XSCBJCBQMSTT798CYG363481MASXGH5W73G24D2F9C7J76YZ2644PGQ6346XBYDXW7Z61JJZN2C2Y8152NNKW3NB0DHTMKHZ5BR"
+
+                )
+            ),
+            PlanchetVector(
+                CreationRequest(
+                    denomPub = 
"040000YE5QYTJTCYF7YDWN2ECYAMBNENHQT7YT740XNC88V5F1K4YC2QD94WABBVHZY597B2BTGBD2NJJV028JKJTD1KBPKXF4D87B7ZJYZVQSA4ZB5H1FVPE7X7YQVG668YZ2YY229X29NM4B6QR0G3TH821QBT1J5EDPKS0RP8E6X4654DTAAYBEN14H96E8D1JFVE40773FVVPXXMX7ZXT7TCVC2EZFMZR1HQ2DDXD8KJZ9AEGS1YH4D629Y08T9X2533MS6R4X58VVKHN1YQVKJT2044A0S8B4AKCW2GJHMQM10XC3K7C3D1C841A6R96GRXPC02QVBQSA1D5VY5VG2T4HVC6NKVK5WAXDEYZNKYVPD9AV4MNCYGK23AZWGHX5E16BQTNG47C9DEETP2D87XFC9D04002",
+                    feeWithdraw = 
Amount.fromJSONString("cRai6j:32749022734.44771"),
+                    reservePriv = 
"H58V73J73HXTA9CPT8ZZ5G7VVKCWZFAE1TMCCV176QBQTPBB2H40",
+                    reservePub = 
"G3R433316Z9PW1H8XRSATJWZJNMKPZ3EE20Z386X7CYM29JDFE0G",
+                    value = 
Amount.fromJSONString("cRai6j:166032749022734.69444771")
+                ),
+                EddsaKeyPair(
+                    
Base32Crockford.decode("5QNA3FX8NA7SETDNEEFJK5W3MNP8AJ8WSBY8FYDVZEYQ1BD21EW0"),
+                    
Base32Crockford.decode("54NDT04NA3TRA38T0D8TMR52PH1EWQP2S4J279GMQWQHKN4W9850")
+                ),
+                "7EKNT64GV5MX0KHZZNB1NREPWCZ7KF9K815M8CQN3B8AKJYF1JV0",
+                CreationResult(
+                    blindingKey = 
"7EKNT64GV5MX0KHZZNB1NREPWCZ7KF9K815M8CQN3B8AKJYF1JV0",
+                    coinEv = 
"AATPF1TXN84PV5P7HE7274B7KT525MFRSPT62MDNYXJXJ2TDGKTMNGPJRH6CMWBD3QQENAEFNS7CZ7P27CBFN6W3EFCFNAS12EWGM6GTTV643RH3A5YJA2R93G0PZPXW9HZP3KZZYFG6MGCRHMHEXTA7T5WKVH6KWE9SM64X9SVKV856VY7TPPWZ0MKZV24KF6TDJ9QC74D2X2FEBDSK7CEA870JENBXC7PZZWJDN8CVN1ZDY4Q0SV8Y4B0YX6CZZ6KVX10PXW56FQ4SSP34EBZCPXCHRZPCQCQRAJ78H4GBP8Y8394QQV1TRH35JQ20R98JSH0WFNAMPQZ246QY8MRFTAT816EY7FEX74ENNKX8494K476BN9VM6CJ5CD0FZYRFSR7DRC5RG9V84SK71EXEDR",
+                    coinPriv = 
"5QNA3FX8NA7SETDNEEFJK5W3MNP8AJ8WSBY8FYDVZEYQ1BD21EW0",
+                    coinPub = 
"54NDT04NA3TRA38T0D8TMR52PH1EWQP2S4J279GMQWQHKN4W9850",
+                    coinValue = 
Amount.fromJSONString("cRai6j:166032749022734.69444771"),
+                    denomPub = 
"040000YE5QYTJTCYF7YDWN2ECYAMBNENHQT7YT740XNC88V5F1K4YC2QD94WABBVHZY597B2BTGBD2NJJV028JKJTD1KBPKXF4D87B7ZJYZVQSA4ZB5H1FVPE7X7YQVG668YZ2YY229X29NM4B6QR0G3TH821QBT1J5EDPKS0RP8E6X4654DTAAYBEN14H96E8D1JFVE40773FVVPXXMX7ZXT7TCVC2EZFMZR1HQ2DDXD8KJZ9AEGS1YH4D629Y08T9X2533MS6R4X58VVKHN1YQVKJT2044A0S8B4AKCW2GJHMQM10XC3K7C3D1C841A6R96GRXPC02QVBQSA1D5VY5VG2T4HVC6NKVK5WAXDEYZNKYVPD9AV4MNCYGK23AZWGHX5E16BQTNG47C9DEETP2D87XFC9D04002",
+                    denomPubHash = 
"RJKMJ93AJ0NYC7X514FPVJ82ST4GW6WZKGK64R69880XBMMGE7H7R8QW71FGWCTKD3KZPW4D3QM854M4YHMYSZ5K3YEA2S7B2GJ9XTR",
+                    reservePub = 
"G3R433316Z9PW1H8XRSATJWZJNMKPZ3EE20Z386X7CYM29JDFE0G",
+                    withdrawSig = 
"X2015X2KE7Z0Q407QEKQ01TKBVV62QT07V9GJGP8GYH04K09TATB9KJG5K4VZG72Y79M1SM1EETVPARSETMN0J7Q057RB6V2F2B2P1G",
+                    coinEvHash = 
"DZ0TEHNTRCXQB3YDZNQYGA0S4RRNKD96Y0PKMG9QQX1KD534RPNRW526CQ5FWESKDT8AJ8R79A9TD20V3JJG3ZQ5JJCMPK9DTF3A8B0"
+                )
+            )
+        )
+        for (v in vectors) testPlanchetVector(v)
+    }
+
+    private fun testPlanchetVector(v: PlanchetVector) {
+        // test vector should match expected result
+        val blindingFactor = Base32Crockford.decode(v.blindingFactor)
+        assertEquals(v.result, planchet.create(v.request, v.eddsaKeyPair, 
blindingFactor))
+
+        // different value should produce different signature
+        val diffValue = v.request.value - Amount.min(v.request.value.currency)
+        val requestDiffValue = v.request.copy(value = diffValue)
+        val requestDiffResult = v.result.copy(coinValue = diffValue)
+        val result = planchet.create(requestDiffValue, v.eddsaKeyPair, 
blindingFactor)
+        assertNotEquals(v.result.withdrawSig, result.withdrawSig)
+        assertNotEquals(requestDiffResult, result)
+
+        // different fee should produce different signature
+        val diffFee = v.request.feeWithdraw - 
Amount.min(v.request.feeWithdraw.currency)
+        val requestDiffFee = v.request.copy(feeWithdraw = diffFee)
+        val resultDiffFee = planchet.create(requestDiffFee, v.eddsaKeyPair, 
blindingFactor)
+        assertNotEquals(v.result.withdrawSig, resultDiffFee.withdrawSig)
+        assertNotEquals(v.result, resultDiffFee)
+
+        // different blinding factor should change result
+        val diffBlindingFactor = Random.nextBytes(32)
+        assertNotEquals(v.result, planchet.create(v.request, v.eddsaKeyPair, 
diffBlindingFactor))
+
+        // different coin keys should change result
+        val diffEddsaKeyPair = crypto.createEddsaKeyPair()
+        assertNotEquals(v.result, planchet.create(v.request, diffEddsaKeyPair, 
blindingFactor))
+    }
+
+}
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Amount.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/Amount.kt
new file mode 100644
index 0000000..a078089
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Amount.kt
@@ -0,0 +1,194 @@
+/*
+ * 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.crypto.CryptoImpl.Companion.toByteArray
+import kotlin.math.floor
+import kotlin.math.pow
+import kotlin.math.roundToInt
+
+class AmountParserException(msg: String? = null, cause: Throwable? = null) : 
Exception(msg, cause)
+class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : 
Exception(msg, cause)
+
+data class Amount(
+    /**
+     * name of the currency using either a three-character ISO 4217 currency 
code,
+     * or a regional currency identifier starting with a "*" followed by at 
most 10 characters.
+     * ISO 4217 exponents in the name are not supported,
+     * although the "fraction" is corresponds to an ISO 4217 exponent of 6.
+     */
+    val currency: String,
+
+    /**
+     * The integer part may be at most 2^52.
+     * Note that "1" here would correspond to 1 EUR or 1 USD, depending on 
currency, not 1 cent.
+     */
+    val value: Long,
+
+    /**
+     * Unsigned 32 bit fractional value to be added to value representing
+     * an additional currency fraction, in units of one hundred millionth 
(1e-8)
+     * of the base currency value.  For example, a fraction
+     * of 50_000_000 would correspond to 50 cents.
+     */
+    val fraction: Int
+) : Comparable<Amount> {
+
+    companion object {
+
+        private const val FRACTIONAL_BASE: Int = 100000000 // 1e8
+
+        @Suppress("unused")
+        private val REGEX = 
Regex("""^[-_*A-Za-z0-9]{1,12}:([0-9]+)\.?([0-9]+)?$""")
+        private val REGEX_CURRENCY = Regex("""^[-_*A-Za-z0-9]{1,12}$""")
+        internal val MAX_VALUE = 2.0.pow(52).toLong()
+        private const val MAX_FRACTION_LENGTH = 8
+        internal const val MAX_FRACTION = 99_999_999
+
+        fun zero(currency: String): Amount {
+            return Amount(checkCurrency(currency), 0, 0)
+        }
+
+        fun fromJSONString(str: String): Amount {
+            val split = str.split(":")
+            if (split.size != 2) throw AmountParserException("Invalid Amount 
Format")
+            return fromString(split[0], split[1])
+        }
+
+        fun fromString(currency: String, str: String): Amount {
+            // value
+            val valueSplit = str.split(".")
+            val value = checkValue(valueSplit[0].toLongOrNull())
+            // fraction
+            val fraction: Int = if (valueSplit.size > 1) {
+                val fractionStr = valueSplit[1]
+                if (fractionStr.length > MAX_FRACTION_LENGTH)
+                    throw AmountParserException("Fraction $fractionStr too 
long")
+                val fraction = "0.$fractionStr".toDoubleOrNull()
+                    ?.times(FRACTIONAL_BASE)
+                    ?.roundToInt()
+                checkFraction(fraction)
+            } else 0
+            return Amount(checkCurrency(currency), value, fraction)
+        }
+
+        fun min(currency: String): Amount = Amount(currency, 0, 1)
+        fun max(currency: String): Amount = Amount(currency, MAX_VALUE, 
MAX_FRACTION)
+
+//        fun fromJsonObject(json: JSONObject): Amount {
+//            val currency = checkCurrency(json.optString("currency"))
+//            val value = checkValue(json.optString("value").toLongOrNull())
+//            val fraction = 
checkFraction(json.optString("fraction").toIntOrNull())
+//            return Amount(currency, value, fraction)
+//        }
+
+        private fun checkCurrency(currency: String): String {
+            if (!REGEX_CURRENCY.matches(currency))
+                throw AmountParserException("Invalid currency: $currency")
+            return currency
+        }
+
+        private fun checkValue(value: Long?): Long {
+            if (value == null || value > MAX_VALUE)
+                throw AmountParserException("Value $value greater than 
$MAX_VALUE")
+            return value
+        }
+
+        private fun checkFraction(fraction: Int?): Int {
+            if (fraction == null || fraction > MAX_FRACTION)
+                throw AmountParserException("Fraction $fraction greater than 
$MAX_FRACTION")
+            return fraction
+        }
+
+    }
+
+    val amountStr: String
+        get() = if (fraction == 0) "$value" else {
+            var f = fraction
+            var fractionStr = ""
+            while (f > 0) {
+                fractionStr += f / (FRACTIONAL_BASE / 10)
+                f = (f * 10) % FRACTIONAL_BASE
+            }
+            "$value.$fractionStr"
+        }
+
+    operator fun plus(other: Amount): Amount {
+        check(currency == other.currency) { "Can only subtract from same 
currency" }
+        val resultValue = value + other.value + floor((fraction + 
other.fraction).toDouble() / FRACTIONAL_BASE).toLong()
+        if (resultValue > MAX_VALUE)
+            throw AmountOverflowException()
+        val resultFraction = (fraction + other.fraction) % FRACTIONAL_BASE
+        return Amount(currency, resultValue, resultFraction)
+    }
+
+    operator fun times(factor: Int): Amount {
+        var result = this
+        for (i in 1 until factor) result += this
+        return result
+    }
+
+    operator fun minus(other: Amount): Amount {
+        check(currency == other.currency) { "Can only subtract from same 
currency" }
+        var resultValue = value
+        var resultFraction = fraction
+        if (resultFraction < other.fraction) {
+            if (resultValue < 1L)
+                throw AmountOverflowException()
+            resultValue--
+            resultFraction += FRACTIONAL_BASE
+        }
+        check(resultFraction >= other.fraction)
+        resultFraction -= other.fraction
+        if (resultValue < other.value)
+            throw AmountOverflowException()
+        resultValue -= other.value
+        return Amount(currency, resultValue, resultFraction)
+    }
+
+    fun isZero(): Boolean {
+        return value == 0L && fraction == 0
+    }
+
+    fun toJSONString(): String {
+        return "$currency:$amountStr"
+    }
+
+    fun toByteArray() = ByteArray(8 + 4 + 12).apply {
+        value.toByteArray().copyInto(this, 0, 0, 8)
+        fraction.toByteArray().copyInto(this, 8, 0, 4)
+        currency.encodeToByteArray().copyInto(this, 12)
+    }
+
+    override fun toString(): String {
+        return "$amountStr $currency"
+    }
+
+    override fun compareTo(other: Amount): Int {
+        check(currency == other.currency) { "Can only compare amounts with the 
same currency" }
+        when {
+            value == other.value -> {
+                if (fraction < other.fraction) return -1
+                if (fraction > other.fraction) return 1
+                return 0
+            }
+            value < other.value -> return -1
+            else -> return 1
+        }
+    }
+
+}
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt
new file mode 100644
index 0000000..fbd6c65
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt
@@ -0,0 +1,27 @@
+package net.taler.wallet.kotlin
+
+import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
+
+
+class Timestamp(
+    // @JsonProperty("t_ms")
+    val ms: Long
+) {
+
+    companion object {
+        const val NEVER: Long = -1
+    }
+
+    /**
+     * Returns a copy of this [Timestamp] rounded to seconds.
+     */
+    fun truncateSeconds(): Timestamp {
+        if (ms == NEVER) return Timestamp(ms)
+        return Timestamp((ms / 1000L) * 1000L)
+    }
+
+    fun roundedToByteArray(): ByteArray = ByteArray(8).apply {
+        (truncateSeconds().ms * 1000L).toByteArray().copyInto(this)
+    }
+
+}
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Crypto.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Crypto.kt
index 620dd84..7019310 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Crypto.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Crypto.kt
@@ -3,6 +3,7 @@ package net.taler.wallet.kotlin.crypto
 internal interface Crypto {
     fun sha256(input: ByteArray): ByteArray
     fun sha512(input: ByteArray): ByteArray
+    fun getRandomBytes(num: Int): ByteArray
     fun eddsaGetPublic(eddsaPrivateKey: ByteArray): ByteArray
     fun ecdheGetPublic(ecdhePrivateKey: ByteArray): ByteArray
     fun createEddsaKeyPair(): EddsaKeyPair
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoImpl.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoImpl.kt
index 98ee656..e6995b1 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoImpl.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoImpl.kt
@@ -1,7 +1,28 @@
 package net.taler.wallet.kotlin.crypto
 
+import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
+
 abstract class CryptoImpl : Crypto {
 
+    companion object {
+        fun Int.toByteArray(): ByteArray {
+            val bytes = ByteArray(4)
+            bytes[3] = (this and 0xFFFF).toByte()
+            bytes[2] = ((this ushr 8) and 0xFFFF).toByte()
+            bytes[1] = ((this ushr 16) and 0xFFFF).toByte()
+            bytes[0] = ((this ushr 24) and 0xFFFF).toByte()
+            return bytes
+        }
+
+        fun Long.toByteArray() = ByteArray(8).apply {
+            var l = this@toByteArray
+            for (i in 7 downTo 0) {
+                this[i] = (l and 0xFF).toByte()
+                l = l shr 8
+            }
+        }
+    }
+
     override fun kdf(outputLength: Int, ikm: ByteArray, salt: ByteArray, info: 
ByteArray): ByteArray {
         return Kdf.kdf(outputLength, ikm, salt, info, { sha256(it) }, { 
sha512(it) })
     }
@@ -15,13 +36,4 @@ abstract class CryptoImpl : Crypto {
         return FreshCoin(eddsaGetPublic(coinPrivateKey), coinPrivateKey, bks)
     }
 
-    private fun Int.toByteArray(): ByteArray {
-        val bytes = ByteArray(4)
-        bytes[3] = (this and 0xFFFF).toByte()
-        bytes[2] = ((this ushr 8) and 0xFFFF).toByte()
-        bytes[1] = ((this ushr 16) and 0xFFFF).toByte()
-        bytes[0] = ((this ushr 24) and 0xFFFF).toByte()
-        return bytes
-    }
-
 }
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt
new file mode 100644
index 0000000..617441d
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt
@@ -0,0 +1,68 @@
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+
+internal class Planchet(private val crypto: Crypto) {
+
+    data class CreationRequest(
+        val value: Amount,
+        val feeWithdraw: Amount,
+        val denomPub: String,
+        val reservePub: String,
+        val reservePriv: String
+    )
+
+    data class CreationResult(
+        val coinPub: String,
+        val coinPriv: String,
+        val reservePub: String,
+        val denomPubHash: String,
+        val denomPub: String,
+        val blindingKey: String,
+        val withdrawSig: String,
+        val coinEv: String,
+        val coinValue: Amount,
+        val coinEvHash: String
+    )
+
+    fun create(req: CreationRequest, coinKeyPair: EddsaKeyPair, 
blindingFactor: ByteArray): CreationResult {
+        val reservePub = Base32Crockford.decode(req.reservePub)
+        val reservePriv = Base32Crockford.decode(req.reservePriv)
+        val denomPub = Base32Crockford.decode(req.denomPub)
+        val coinPubHash = crypto.sha512(coinKeyPair.publicKey)
+        val ev = crypto.rsaBlind(coinPubHash, blindingFactor, denomPub)
+        val amountWithFee = req.value + req.feeWithdraw
+        val denomPubHash = crypto.sha512(denomPub)
+        val evHash = crypto.sha512(ev)
+
+        val withdrawRequest = 
Signature.PurposeBuilder(Signature.RESERVE_WITHDRAW)
+            .put(reservePub)
+            .put(amountWithFee.toByteArray())
+            .put(req.feeWithdraw.toByteArray())
+            .put(denomPubHash)
+            .put(evHash)
+            .build()
+
+        val sig = crypto.eddsaSign(withdrawRequest, reservePriv)
+        return CreationResult(
+            blindingKey = Base32Crockford.encode(blindingFactor),
+            coinEv = Base32Crockford.encode(ev),
+            coinPriv = Base32Crockford.encode(coinKeyPair.privateKey),
+            coinPub = Base32Crockford.encode(coinKeyPair.publicKey),
+            coinValue = req.value,
+            denomPub = req.denomPub,
+            denomPubHash = Base32Crockford.encode(denomPubHash),
+            reservePub = req.reservePub,
+            withdrawSig = Base32Crockford.encode(sig),
+            coinEvHash = Base32Crockford.encode(evHash)
+        )
+    }
+
+    fun create(req: CreationRequest): CreationResult {
+        val coinKeyPair = crypto.createEddsaKeyPair()
+        val blindingFactor = crypto.getRandomBytes(32)
+        return create(req, coinKeyPair, blindingFactor)
+    }
+
+}
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt
new file mode 100644
index 0000000..30db04f
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt
@@ -0,0 +1,46 @@
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
+
+class Signature {
+
+    companion object {
+        const val RESERVE_WITHDRAW = 1200
+        const val WALLET_COIN_DEPOSIT = 1201
+        const val MASTER_DENOMINATION_KEY_VALIDITY = 1025
+        const val MASTER_WIRE_FEES = 1028
+        const val MASTER_WIRE_DETAILS = 1030
+        const val WALLET_COIN_MELT = 1202
+        const val TEST = 4242
+        const val MERCHANT_PAYMENT_OK = 1104
+        const val WALLET_COIN_RECOUP = 1203
+        const val WALLET_COIN_LINK = 1204
+        const val EXCHANGE_CONFIRM_RECOUP = 1039
+        const val EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041
+    }
+
+    internal class PurposeBuilder(private val purposeNum: Int) {
+        private val chunks = ArrayList<ByteArray>()
+
+        fun put(bytes: ByteArray): PurposeBuilder {
+            chunks.add(bytes)
+            return this
+        }
+
+        fun build(): ByteArray {
+            var payloadLen = 0
+            for (c in chunks) payloadLen += c.size
+            val size = 4 + 4 + payloadLen
+            val bytes = ByteArray(size)
+            size.toByteArray().copyInto(bytes, 0)
+            purposeNum.toByteArray().copyInto(bytes, 4)
+            var offset = 8
+            for (c in chunks) {
+                c.copyInto(bytes, offset)
+                offset += c.size
+            }
+            return bytes
+        }
+    }
+
+}
diff --git a/src/commonTest/kotlin/net/taler/wallet/kotlin/AmountTest.kt 
b/src/commonTest/kotlin/net/taler/wallet/kotlin/AmountTest.kt
new file mode 100644
index 0000000..578874d
--- /dev/null
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/AmountTest.kt
@@ -0,0 +1,270 @@
+/*
+ * 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: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/src/commonTest/kotlin/net/taler/wallet/kotlin/TimestampTest.kt 
b/src/commonTest/kotlin/net/taler/wallet/kotlin/TimestampTest.kt
new file mode 100644
index 0000000..1a12549
--- /dev/null
+++ b/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/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt 
b/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt
new file mode 100644
index 0000000..1326cc4
--- /dev/null
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt
@@ -0,0 +1,184 @@
+/*
+ * 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.Signature.PurposeBuilder
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class SignatureTest {
+
+    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)
+        }
+    }
+
+}
diff --git a/src/jsMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt 
b/src/jsMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
index 2602244..7cf210a 100644
--- a/src/jsMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
+++ b/src/jsMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
@@ -17,6 +17,10 @@ internal object CryptoJsImpl : CryptoImpl() {
         return nacl.hash(input.toUint8Array()).toByteArray()
     }
 
+    override fun getRandomBytes(num: Int): ByteArray {
+        return nacl.randomBytes(num).toByteArray()
+    }
+
     override fun eddsaGetPublic(eddsaPrivateKey: ByteArray): ByteArray {
         val pair = nacl.sign.keyPair.fromSeed(eddsaPrivateKey.toUint8Array())
         return pair.publicKey.toByteArray()
diff --git 
a/src/linuxMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt 
b/src/linuxMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
index 40a11ce..a44bc46 100644
--- a/src/linuxMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
+++ b/src/linuxMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt
@@ -41,6 +41,12 @@ internal object CryptoNativeImpl : CryptoImpl() {
         return output
     }
 
+    override fun getRandomBytes(num: Int): ByteArray {
+        val bytes = ByteArray(num)
+        randombytes(bytes.toCValuesRef(), num.toULong())
+        return bytes
+    }
+
     override fun eddsaGetPublic(eddsaPrivateKey: ByteArray): ByteArray {
         val publicKey = ByteArray(crypto_sign_PUBLICKEYBYTES.toInt())
         val privateKey = ByteArray(crypto_sign_SECRETKEYBYTES.toInt())

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]