summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.idea/dictionaries/user.xml1
-rw-r--r--src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt4
-rw-r--r--src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/PlanchetTest.kt125
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Crypto.kt1
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Planchet.kt68
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt26
-rw-r--r--src/jsMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt4
-rw-r--r--src/linuxMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt6
8 files changed, 222 insertions, 13 deletions
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/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())