diff options
Diffstat (limited to 'wallet/src/commonMain/kotlin/net/taler/lib/wallet/crypto')
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) + } + +} |