summaryrefslogtreecommitdiff
path: root/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto
diff options
context:
space:
mode:
Diffstat (limited to 'wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto')
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Crypto.kt78
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/CryptoImpl.kt53
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Deposit.kt112
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Kdf.kt90
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Planchet.kt88
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Recoup.kt83
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Refresh.kt266
-rw-r--r--wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Signature.kt157
8 files changed, 927 insertions, 0 deletions
diff --git a/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Crypto.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Crypto.kt
new file mode 100644
index 0000000..cbb486a
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Crypto.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.lib.wallet.crypto
+
+internal interface Crypto {
+ fun sha256(input: ByteArray): ByteArray
+ fun sha512(input: ByteArray): ByteArray
+ fun getHashSha512State(): HashSha512State
+ fun getRandomBytes(num: Int): ByteArray
+ fun eddsaGetPublic(eddsaPrivateKey: ByteArray): ByteArray
+ fun ecdheGetPublic(ecdhePrivateKey: ByteArray): ByteArray
+ fun createEddsaKeyPair(): EddsaKeyPair
+ fun createEcdheKeyPair(): EcdheKeyPair
+ fun eddsaSign(msg: ByteArray, eddsaPrivateKey: ByteArray): ByteArray
+ fun eddsaVerify(msg: ByteArray, sig: ByteArray, eddsaPub: ByteArray): Boolean
+ fun keyExchangeEddsaEcdhe(eddsaPrivateKey: ByteArray, ecdhePublicKey: ByteArray): ByteArray
+ fun keyExchangeEcdheEddsa(ecdhePrivateKey: ByteArray, eddsaPublicKey: ByteArray): ByteArray
+ fun kdf(outputLength: Int, ikm: ByteArray, salt: ByteArray, info: ByteArray): ByteArray
+ fun rsaBlind(hm: ByteArray, bks: ByteArray, rsaPubEnc: ByteArray): ByteArray
+ fun rsaUnblind(sig: ByteArray, rsaPubEnc: ByteArray, bks: ByteArray): ByteArray
+ fun rsaVerify(hm: ByteArray, rsaSig: ByteArray, rsaPubEnc: ByteArray): Boolean
+ fun setupRefreshPlanchet(secretSeed: ByteArray, coinNumber: Int): FreshCoin
+}
+
+interface HashSha512State {
+ fun update(data: ByteArray): HashSha512State
+ fun final(): ByteArray
+}
+class EddsaKeyPair(val privateKey: ByteArray, val publicKey: ByteArray)
+class EcdheKeyPair(val privateKey: ByteArray, val publicKey: ByteArray)
+data class FreshCoin(val coinPublicKey: ByteArray, val coinPrivateKey: ByteArray, val bks: ByteArray) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+ other as FreshCoin
+ if (!coinPublicKey.contentEquals(other.coinPublicKey)) return false
+ if (!coinPrivateKey.contentEquals(other.coinPrivateKey)) return false
+ if (!bks.contentEquals(other.bks)) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = coinPublicKey.contentHashCode()
+ result = 31 * result + coinPrivateKey.contentHashCode()
+ result = 31 * result + bks.contentHashCode()
+ return result
+ }
+}
+
+internal expect object CryptoFactory {
+ internal fun getCrypto(): Crypto
+}
+
+private val hexArray = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
+
+fun ByteArray.toHexString(): String {
+ val hexChars = CharArray(this.size * 2)
+ for (j in this.indices) {
+ val v = (this[j].toInt() and 0xFF)
+ hexChars[j * 2] = hexArray[v ushr 4]
+ hexChars[j * 2 + 1] = hexArray[v and 0x0F]
+ }
+ return String(hexChars)
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/CryptoImpl.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/CryptoImpl.kt
new file mode 100644
index 0000000..6b7cb8e
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/CryptoImpl.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.lib.wallet.crypto
+
+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) })
+ }
+
+ override fun setupRefreshPlanchet(secretSeed: ByteArray, coinNumber: Int): FreshCoin {
+ val info = "taler-coin-derivation".encodeToByteArray()
+ val salt = coinNumber.toByteArray()
+ val out = kdf(64, secretSeed, salt, info)
+ val coinPrivateKey = out.copyOfRange(0, 32)
+ val bks = out.copyOfRange(32, 64)
+ return FreshCoin(eddsaGetPublic(coinPrivateKey), coinPrivateKey, bks)
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Deposit.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Deposit.kt
new file mode 100644
index 0000000..66255d8
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Deposit.kt
@@ -0,0 +1,112 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.crypto
+
+import net.taler.lib.common.Amount
+import net.taler.lib.common.Timestamp
+import net.taler.lib.wallet.Base32Crockford
+import net.taler.lib.wallet.crypto.Signature.Companion.WALLET_COIN_DEPOSIT
+import net.taler.lib.wallet.roundedToByteArray
+import net.taler.lib.wallet.toByteArray
+
+/**
+ * Deposit operations are requested by a merchant during a transaction.
+ * For the deposit operation, the merchant has to obtain the deposit permission for a coin
+ * from their customer who owns the coin.
+ *
+ * When depositing a coin, the merchant is credited an amount specified in the deposit permission,
+ * possibly a fraction of the total coin’s value,
+ * minus the deposit fee as specified by the coin’s denomination.
+ */
+internal class Deposit(private val crypto: Crypto) {
+
+ /**
+ * Private data required to make a deposit permission.
+ */
+ data class DepositInfo(
+ val exchangeBaseUrl: String,
+ val contractTermsHash: String,
+ val coinPublicKey: String,
+ val coinPrivateKey: String,
+ val spendAmount: Amount,
+ val timestamp: Timestamp,
+ val refundDeadline: Timestamp,
+ val merchantPublicKey: String,
+ val feeDeposit: Amount,
+ val wireInfoHash: String,
+ val denomPublicKey: String,
+ val denomSignature: String
+ )
+
+ /**
+ * Deposit permission for a single coin.
+ */
+ // TODO rename _
+ data class CoinDepositPermission(
+ /**
+ * Signature by the coin.
+ */
+ val coinSignature: String,
+ /**
+ * Public key of the coin being spend.
+ */
+ val coinPublicKey: String,
+ /**
+ * Signature made by the denomination public key.
+ */
+ val denomSignature: String,
+ /**
+ * The denomination public key associated with this coin.
+ */
+ val denomPublicKey: String,
+ /**
+ * The amount that is subtracted from this coin with this payment.
+ */
+ val contribution: String,
+ /**
+ * URL of the exchange this coin was withdrawn from.
+ */
+ val exchangeBaseUrl: String
+ )
+
+ /**
+ * Generate updated coins (to store in the database) and deposit permissions for each given coin.
+ */
+ fun signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
+ val d = Signature.PurposeBuilder(WALLET_COIN_DEPOSIT)
+ .put(Base32Crockford.decode(depositInfo.contractTermsHash))
+ .put(Base32Crockford.decode(depositInfo.wireInfoHash))
+ .put(depositInfo.timestamp.roundedToByteArray())
+ .put(depositInfo.refundDeadline.roundedToByteArray())
+ .put(depositInfo.spendAmount.toByteArray())
+ .put(depositInfo.feeDeposit.toByteArray())
+ .put(Base32Crockford.decode(depositInfo.merchantPublicKey))
+ .put(Base32Crockford.decode(depositInfo.coinPublicKey))
+ .build()
+ val coinPriv = Base32Crockford.decode(depositInfo.coinPrivateKey);
+ val coinSig = crypto.eddsaSign(d, coinPriv)
+ return CoinDepositPermission(
+ coinPublicKey = depositInfo.coinPublicKey,
+ coinSignature = Base32Crockford.encode(coinSig),
+ contribution = depositInfo.spendAmount.toJSONString(),
+ denomPublicKey = depositInfo.denomPublicKey,
+ exchangeBaseUrl = depositInfo.exchangeBaseUrl,
+ denomSignature = depositInfo.denomSignature
+ )
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Kdf.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Kdf.kt
new file mode 100644
index 0000000..2d714f8
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Kdf.kt
@@ -0,0 +1,90 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.crypto
+
+import kotlin.experimental.xor
+import kotlin.math.ceil
+
+internal object Kdf {
+
+ const val HMAC_SHA256_BLOCK_SIZE = 64
+ const val HMAC_SHA512_BLOCK_SIZE = 128
+
+ fun kdf(
+ outputLength: Int,
+ ikm: ByteArray,
+ salt: ByteArray,
+ info: ByteArray,
+ sha256: (ByteArray) -> ByteArray,
+ sha512: (ByteArray) -> ByteArray
+ ): ByteArray {
+ //extract
+ val prk = hmacSha512(salt, ikm, sha512)
+
+ // expand
+ val n = ceil(outputLength.toDouble() / 32).toInt()
+ val output = ByteArray(n * 32)
+ for (i in 0 until n) {
+ val buf: ByteArray
+ if (i == 0) {
+ buf = ByteArray(info.size + 1)
+ info.copyInto(buf)
+ } else {
+ buf = ByteArray(info.size + 1 + 32)
+ for (j in 0 until 32) {
+ buf[j] = output[(i - 1) * 32 + j]
+ }
+ info.copyInto(buf, destinationOffset = 32)
+ }
+ buf[buf.size - 1] = (i + 1).toByte()
+ val chunk = hmacSha256(prk, buf, sha256)
+ chunk.copyInto(output, destinationOffset = i * 32)
+ }
+ return output.copyOfRange(0, outputLength)
+ }
+
+ fun hmacSha256(key: ByteArray, message: ByteArray, sha256: (ByteArray) -> ByteArray): ByteArray {
+ return hmac(HMAC_SHA256_BLOCK_SIZE, key, message) { sha256(it) }
+ }
+
+ fun hmacSha512(key: ByteArray, message: ByteArray, sha512: (ByteArray) -> ByteArray): ByteArray {
+ return hmac(HMAC_SHA512_BLOCK_SIZE, key, message) { sha512(it) }
+ }
+
+ private fun hmac(blockSize: Int, key: ByteArray, message: ByteArray, hash: (ByteArray) -> ByteArray): ByteArray {
+ var newKey = key
+ if (newKey.size > blockSize) newKey = hash(newKey)
+ if (newKey.size < blockSize) newKey = ByteArray(blockSize).apply {
+ newKey.copyInto(this)
+ }
+ val okp = ByteArray(blockSize)
+ val ikp = ByteArray(blockSize)
+ for (i in 0 until blockSize) {
+ ikp[i] = newKey[i] xor 0x36
+ okp[i] = newKey[i] xor 0x5c
+ }
+ val b1 = ByteArray(blockSize + message.size)
+ ikp.copyInto(b1)
+ message.copyInto(b1, destinationOffset = blockSize)
+ val h0 = hash(b1)
+ val b2 = ByteArray(blockSize + h0.size)
+ okp.copyInto(b2)
+ h0.copyInto(b2, destinationOffset = blockSize)
+ return hash(b2)
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Planchet.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Planchet.kt
new file mode 100644
index 0000000..22aa786
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Planchet.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.lib.wallet.crypto
+
+import net.taler.lib.common.Amount
+import net.taler.lib.wallet.Base32Crockford
+import net.taler.lib.wallet.toByteArray
+
+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
+ )
+
+ internal 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)
+ )
+ }
+
+ /**
+ * Create a pre-coin ([Planchet]) of the given [CreationRequest].
+ */
+ fun create(req: CreationRequest): CreationResult {
+ val coinKeyPair = crypto.createEddsaKeyPair()
+ val blindingFactor = crypto.getRandomBytes(32)
+ return create(req, coinKeyPair, blindingFactor)
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Recoup.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Recoup.kt
new file mode 100644
index 0000000..c7678e4
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Recoup.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.lib.wallet.crypto
+
+import net.taler.lib.wallet.Base32Crockford
+import net.taler.lib.wallet.CoinRecord
+import net.taler.lib.wallet.CoinSourceType.REFRESH
+import net.taler.lib.wallet.crypto.Signature.Companion.WALLET_COIN_RECOUP
+
+internal class Recoup(private val crypto: Crypto) {
+
+ /**
+ * Request that we send to the exchange to get a payback.
+ */
+ data class Request(
+ /**
+ * Hashed denomination public key of the coin we want to get
+ * paid back.
+ */
+ val denomPubHash: String,
+
+ /**
+ * Signature over the coin public key by the denomination.
+ */
+ val denomSig: String,
+
+ /**
+ * Coin public key of the coin we want to refund.
+ */
+ val coinPub: String,
+
+ /**
+ * Blinding key that was used during withdraw,
+ * used to prove that we were actually withdrawing the coin.
+ */
+ val coinBlindKeySecret: String,
+
+ /**
+ * Signature made by the coin, authorizing the payback.
+ */
+ val coinSig: String,
+
+ /**
+ * Was the coin refreshed (and thus the recoup should go to the old coin)?
+ */
+ val refreshed: Boolean
+ )
+
+ /**
+ * Create and sign a message to recoup a coin.
+ */
+ fun createRequest(coin: CoinRecord): Request {
+ val p = Signature.PurposeBuilder(WALLET_COIN_RECOUP)
+ .put(Base32Crockford.decode(coin.coinPub))
+ .put(Base32Crockford.decode(coin.denomPubHash))
+ .put(Base32Crockford.decode(coin.blindingKey))
+ .build()
+ val coinSig = crypto.eddsaSign(p, Base32Crockford.decode(coin.coinPriv))
+ return Request(
+ coinBlindKeySecret = coin.blindingKey,
+ coinPub = coin.coinPub,
+ coinSig = Base32Crockford.encode(coinSig),
+ denomPubHash = coin.denomPubHash,
+ denomSig = coin.denomSig,
+ refreshed = coin.coinSource === REFRESH
+ )
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Refresh.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Refresh.kt
new file mode 100644
index 0000000..90478ef
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Refresh.kt
@@ -0,0 +1,266 @@
+/*
+ * 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.lib.wallet.crypto
+
+import net.taler.lib.common.Amount
+import net.taler.lib.wallet.Base32Crockford
+import net.taler.lib.wallet.CoinRecord
+import net.taler.lib.common.Timestamp
+import net.taler.lib.wallet.crypto.Signature.Companion.WALLET_COIN_LINK
+import net.taler.lib.wallet.crypto.Signature.Companion.WALLET_COIN_MELT
+import net.taler.lib.wallet.crypto.Signature.PurposeBuilder
+import net.taler.lib.wallet.exchange.DenominationSelectionInfo
+import net.taler.lib.wallet.exchange.SelectedDenomination
+import net.taler.lib.wallet.toByteArray
+
+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
+ )
+
+ /**
+ * 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)
+ }
+
+}
diff --git a/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Signature.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Signature.kt
new file mode 100644
index 0000000..006c004
--- /dev/null
+++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto/Signature.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.lib.wallet.crypto
+
+import net.taler.lib.wallet.Base32Crockford
+import net.taler.lib.wallet.crypto.CryptoImpl.Companion.toByteArray
+import net.taler.lib.wallet.exchange.DenominationRecord
+import net.taler.lib.wallet.exchange.WireFee
+import net.taler.lib.wallet.roundedToByteArray
+import net.taler.lib.wallet.toByteArray
+
+internal class Signature(private val crypto: Crypto) {
+
+ @Suppress("unused")
+ 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
+ }
+ }
+
+ private fun verifyPayment(sig: ByteArray, contractHash: ByteArray, merchantPub: ByteArray): Boolean {
+ val p = PurposeBuilder(MERCHANT_PAYMENT_OK)
+ .put(contractHash)
+ .build()
+ return crypto.eddsaVerify(p, sig, merchantPub)
+ }
+
+ /**
+ * Verifies an EdDSA payment signature made with [MERCHANT_PAYMENT_OK].
+ *
+ * @param merchantPub an EdDSA public key, usually belonging to a merchant.
+ *
+ * @return true if the signature is valid, false otherwise
+ */
+ fun verifyPayment(sig: String, contractHash: String, merchantPub: String): Boolean {
+ val sigBytes = Base32Crockford.decode(sig)
+ val hashBytes = Base32Crockford.decode(contractHash)
+ val pubBytes = Base32Crockford.decode(merchantPub)
+ return verifyPayment(sigBytes, hashBytes, pubBytes)
+ }
+
+ /**
+ * Verifies an EdDSA wire fee signature made with [MASTER_WIRE_FEES].
+ *
+ * @param masterPub an EdDSA public key
+ *
+ * @return true if the signature is valid, false otherwise
+ */
+ fun verifyWireFee(type: String, wireFee: WireFee, masterPub: String): Boolean {
+ val p = PurposeBuilder(MASTER_WIRE_FEES)
+ .put(crypto.sha512("$type\u0000".encodeToByteArray()))
+ .put(wireFee.startStamp.roundedToByteArray())
+ .put(wireFee.endStamp.roundedToByteArray())
+ .put(wireFee.wireFee.toByteArray())
+ .put(wireFee.closingFee.toByteArray())
+ .build()
+ val sig = Base32Crockford.decode(wireFee.signature)
+ val pub = Base32Crockford.decode(masterPub)
+ return crypto.eddsaVerify(p, sig, pub)
+ }
+
+ /**
+ * Verifies an EdDSA denomination record signature made with [MASTER_DENOMINATION_KEY_VALIDITY].
+ *
+ * @param masterPub an EdDSA public key
+ *
+ * @return true if the signature is valid, false otherwise
+ */
+ fun verifyDenominationRecord(d: DenominationRecord, masterPub: String): Boolean {
+ val pub = Base32Crockford.decode(masterPub)
+ val p = PurposeBuilder(MASTER_DENOMINATION_KEY_VALIDITY)
+ .put(pub)
+ .put(d.stampStart.roundedToByteArray())
+ .put(d.stampExpireWithdraw.roundedToByteArray())
+ .put(d.stampExpireDeposit.roundedToByteArray())
+ .put(d.stampExpireLegal.roundedToByteArray())
+ .put(d.value.toByteArray())
+ .put(d.feeWithdraw.toByteArray())
+ .put(d.feeDeposit.toByteArray())
+ .put(d.feeRefresh.toByteArray())
+ .put(d.feeRefund.toByteArray())
+ .put(Base32Crockford.decode(d.denomPubHash))
+ .build()
+ val sig = Base32Crockford.decode(d.masterSig)
+ return crypto.eddsaVerify(p, sig, pub)
+ }
+
+ /**
+ * Verifies an EdDSA wire account signature made with [MASTER_WIRE_DETAILS].
+ *
+ * @param masterPub an EdDSA public key
+ *
+ * @return true if the signature is valid, false otherwise
+ */
+ fun verifyWireAccount(paytoUri: String, signature: String, masterPub: String): Boolean {
+ val h = crypto.kdf(
+ 64,
+ "exchange-wire-signature".encodeToByteArray(),
+ "$paytoUri\u0000".encodeToByteArray(),
+ ByteArray(0)
+ )
+ val p = PurposeBuilder(MASTER_WIRE_DETAILS)
+ .put(h)
+ .build()
+ val sig = Base32Crockford.decode(signature)
+ val pub = Base32Crockford.decode(masterPub)
+ return crypto.eddsaVerify(p, sig, pub)
+ }
+
+}