diff options
author | Torsten Grote <t@grobox.de> | 2020-06-24 17:53:20 -0300 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2020-06-24 17:53:20 -0300 |
commit | 86bd04302b8691aa3e518a70bafa9d95d8358e82 (patch) | |
tree | a115f4d439b4fc3e97c867a79a167603bc9ed471 /src | |
parent | f6d37f2b3c5465ab445469a08509352eed4b6449 (diff) | |
download | wallet-kotlin-86bd04302b8691aa3e518a70bafa9d95d8358e82.tar.gz wallet-kotlin-86bd04302b8691aa3e518a70bafa9d95d8358e82.tar.bz2 wallet-kotlin-86bd04302b8691aa3e518a70bafa9d95d8358e82.zip |
Add Planchet creation with tests and platform specific secure random bytes
Diffstat (limited to 'src')
7 files changed, 221 insertions, 13 deletions
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/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/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 index 881ace2..30db04f 100644 --- a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt +++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt @@ -4,19 +4,19 @@ import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray class Signature { - internal enum class Purpose(val num: Int) { - RESERVE_WITHDRAW(1200), - WALLET_COIN_DEPOSIT(1201), - MASTER_DENOMINATION_KEY_VALIDITY(1025), - MASTER_WIRE_FEES(1028), - MASTER_WIRE_DETAILS(1030), - WALLET_COIN_MELT(1202), - TEST(4242), - MERCHANT_PAYMENT_OK(1104), - WALLET_COIN_RECOUP(1203), - WALLET_COIN_LINK(1204), - EXCHANGE_CONFIRM_RECOUP(1039), - EXCHANGE_CONFIRM_RECOUP_REFRESH(1041) + 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) { 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()) |