diff options
author | Torsten Grote <t@grobox.de> | 2020-06-05 15:54:20 -0300 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2020-06-05 15:54:20 -0300 |
commit | f25a15c55f4b8cb1bc97aaff68d7468459377934 (patch) | |
tree | e8d5392331df61ebec7b10b5ea733e0856bcf77f /src | |
parent | f37a38dc0fb6a144b8e9ea898331fb8665e602fc (diff) | |
download | wallet-kotlin-f25a15c55f4b8cb1bc97aaff68d7468459377934.tar.gz wallet-kotlin-f25a15c55f4b8cb1bc97aaff68d7468459377934.tar.bz2 wallet-kotlin-f25a15c55f4b8cb1bc97aaff68d7468459377934.zip |
Implement RSA blinding (only JVM)
Diffstat (limited to 'src')
6 files changed, 162 insertions, 3 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 d41b615..721024b 100644 --- a/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt +++ b/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt @@ -72,4 +72,8 @@ internal object CryptoJvmImpl : CryptoImpl() { return sha512(x) } + override fun rsaBlind(hm: ByteArray, bks: ByteArray, rsaPubEnc: ByteArray): ByteArray { + return RsaBlinding.rsaBlind(hm, bks, rsaPubEnc) + } + } diff --git a/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt b/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt new file mode 100644 index 0000000..6877df8 --- /dev/null +++ b/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt @@ -0,0 +1,103 @@ +package net.taler.wallet.kotlin.crypto + +import java.math.BigInteger +import kotlin.math.ceil +import kotlin.math.floor + +@OptIn(ExperimentalStdlibApi::class) +internal object RsaBlinding { + + fun rsaBlind(hm: ByteArray, bks: ByteArray, rsaPubEnc: ByteArray): ByteArray { + val rsaPub = rsaPubDecode(rsaPubEnc) + val data = rsaFullDomainHash(hm, rsaPub) + val r = rsaBlindingKeyDerive(rsaPub, bks) + val rE = r.modPow(rsaPub.e, rsaPub.n) + val bm = rE.multiply(data).mod(rsaPub.n) + return bm.toByteArrayWithoutSign() + } + + private fun rsaBlindingKeyDerive(rsaPub: RsaPublicKey, bks: ByteArray): BigInteger { + val salt = "Blinding KDF extrator HMAC key".encodeToByteArray() + val info = "Blinding KDF".encodeToByteArray() + return kdfMod(rsaPub.n, bks, salt, info) + } + + private fun rsaPubDecode(publicKey: ByteArray): RsaPublicKey { + val modulusLength = (publicKey[0].toInt() shl 8) or publicKey[1].toInt() + val exponentLength = (publicKey[2].toInt() shl 8) or publicKey[3].toInt() + if (4 + exponentLength + modulusLength != publicKey.size) { + throw Error("invalid RSA public key (format wrong)") + } + val modulus = publicKey.copyOfRange(4, 4 + modulusLength) + val exponent = publicKey.copyOfRange( + 4 + modulusLength, + 4 + modulusLength + exponentLength + ) + return RsaPublicKey(BigInteger(1, modulus), BigInteger(1, exponent)) + } + + private fun rsaFullDomainHash(hm: ByteArray, rsaPublicKey: RsaPublicKey): BigInteger { + val info = "RSA-FDA FTpsW!".encodeToByteArray() + val salt = rsaPubEncode(rsaPublicKey) + val r = kdfMod(rsaPublicKey.n, hm, salt, info) + rsaGcdValidate(r, rsaPublicKey.n) + return r + } + + private fun rsaPubEncode(rsaPublicKey: RsaPublicKey): ByteArray { + val mb = rsaPublicKey.n.toByteArrayWithoutSign() + val eb = rsaPublicKey.e.toByteArrayWithoutSign() + val out = ByteArray(4 + mb.size + eb.size) + out[0] = ((mb.size ushr 8) and 0xff).toByte() + out[1] = (mb.size and 0xff).toByte() + out[2] = ((eb.size ushr 8) and 0xff).toByte() + out[3] = (eb.size and 0xff).toByte() + mb.copyInto(out, destinationOffset = 4) + eb.copyInto(out, destinationOffset = 4 + mb.size) + return out + } + + private fun kdfMod(n: BigInteger, ikm: ByteArray, salt: ByteArray, info: ByteArray): BigInteger { + val nBits = n.bitLength() + val bufLen = floor((nBits.toDouble() - 1) / 8 + 1).toInt() + val mask = (1 shl (8 - (bufLen * 8 - nBits))) - 1 + var counter = 0 + while (true) { + val ctx = ByteArray(info.size + 2) + info.copyInto(ctx) + ctx[ctx.size - 2] = ((counter ushr 8) and 0xff).toByte() + ctx[ctx.size - 1] = (counter and 0xff).toByte() + val buf = CryptoJvmImpl.kdf(bufLen, ikm, salt, ctx) + val arr = buf.copyOf() + arr[0] = (arr[0].toInt() and mask).toByte() + val r = BigInteger(1, arr) + if (r < n) return r + counter++ + } + } + + /** + * Test for malicious RSA key. + * + * Assuming n is an RSA modulous and r is generated using a call to + * GNUNET_CRYPTO_kdf_mod_mpi, if gcd(r,n) != 1 then n must be a + * malicious RSA key designed to deanomize the user. + * + * @param r KDF result + * @param n RSA modulus of the public key + */ + private fun rsaGcdValidate(r: BigInteger, n: BigInteger) { + if (r.gcd(n) != BigInteger.ONE) throw Error("malicious RSA public key") + } + + // TODO check that this strips *only* the sign correctly + private fun BigInteger.toByteArrayWithoutSign(): ByteArray = this.toByteArray().let { + val byteLength = ceil(this.bitLength().toDouble() / 8).toInt() + val signBitPosition = ceil((this.bitLength() + 1).toDouble() / 8).toInt() + val start = signBitPosition - byteLength + it.copyOfRange(start, it.size) // stripping least significant byte (sign) + } + +} + +internal class RsaPublicKey(val n: BigInteger, val e: BigInteger) diff --git a/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RsaBlindingTest.kt b/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RsaBlindingTest.kt new file mode 100644 index 0000000..2c96797 --- /dev/null +++ b/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RsaBlindingTest.kt @@ -0,0 +1,29 @@ +package net.taler.wallet.kotlin.crypto + +import net.taler.wallet.kotlin.Base32Crockford +import kotlin.test.Test +import kotlin.test.assertEquals + +// TODO move to commonTest once RsaBlinding is implemented everywhere +class RsaBlindingTest { + + private val crypto = CryptoFactory.getCrypto() + + @Test + fun testBlinding() { + val messageHash = + "TT1R28D79EJEJ9PC35AQS35CCG85DSXSZ508MV2HS2FN4ME6AHESZX5WP485R8A75KG53FN6F1YNW95008663TKAPWB81420VG17BY8" + val rsaPublicKey = + "040000Y62RSDDKZXTE7GDVA302ZZR0DY224RSDT6WDWR1XGT8E3YG80XV6TMT3ZCNP8XC84W0N6MSZ0EF8S3YB1JJ2AXY9JQZW3MCA0CG38ER4YE2RY4Q2666DEZSNKT29V6CKZVCDHXSAKY8W6RPEKEQ5YSBYQK23MRK3CQTNNJXQFDKEMRHEC5Y6RDHAC5RJCV8JJ8BF18VPKZ2Q7BB14YN1HJ22H8EZGW0RDGG9YPEWA9183BHEQ651PP81J514TJ9K8DH23AJ50SZFNS429HQ390VRP5E4MQ7RK7ZJXXTSZAQSRTC0QF28P23PD37C17QFQB0BBC54MB8MDH7RW104STG6VN0J22P39JP4EXPVGK5D9AX5W869MDQ6SRD42ZYK5H20227Q8CCWSQ6C3132WP0F0H04002" + val bks = "7QD31RPJH0W306RJWBRG646Z2FTA1F89BKSXPDAG7YM0N5Z0B610" + val expectedBm = + "GA8PC6YH9VF5MW6P2DKTV0W0ZTQ24DZ9EAN5QH3SQXRH7SCZHFMM21ZY05F0BS7MFW8TSEP4SEB280BYP5ACHNQWGE10PCXDDMK7ECXJDPHJ224JBCV4KYNWG6NBR3SC9HK8FXVFX55GFBJFNQHNZGEB8DB0KN9MSVYFDXN45KPMSNY03FVX0JZ0R3YG9XQ8XVGB5SYZCF0QSHWH61MT0Q10CZD2V114BT64D3GD86EJ5S9WBMYG51SDN5CSKEJ734YAJ4HCEWW0RDN8GXA9ZMA18SKVW8T3TTBCPJRF2Y77JGQ08GF35SYGA2HWFV1HGVS8RCTER6GB9SZHRG4T7919H9C1KFAP50G2KSV6X42D6KNJANNSGKQH649TJ00YJQXPHPNFBSS198RY2C243D4B4W" + val bm = crypto.rsaBlind( + Base32Crockford.decode(messageHash), + Base32Crockford.decode(bks), + Base32Crockford.decode(rsaPublicKey) + ) + assertEquals(expectedBm, Base32Crockford.encode(bm)) + } + +} 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 c5c0a0f..d2a77cc 100644 --- a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Crypto.kt +++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Crypto.kt @@ -12,6 +12,7 @@ internal interface Crypto { 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 } class EddsaKeyPair(val privateKey: ByteArray, val publicKey: ByteArray) 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 2904168..db502df 100644 --- a/src/jsMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt +++ b/src/jsMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt @@ -61,6 +61,10 @@ internal object CryptoJsImpl : CryptoImpl() { return sha512(x) } + override fun rsaBlind(hm: ByteArray, bks: ByteArray, rsaPubEnc: ByteArray): ByteArray { + TODO("Not yet implemented") + } + private fun Uint8Array.toByteArray(): ByteArray { val result = ByteArray(this.length) for (i in 0 until this.length) result[i] = this[i] 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 11c63e7..152e55f 100644 --- a/src/linuxMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt +++ b/src/linuxMain/kotlin/net/taler/wallet/kotlin/crypto/CryptoFactory.kt @@ -3,14 +3,28 @@ package net.taler.wallet.kotlin.crypto import kotlinx.cinterop.CValuesRef import kotlinx.cinterop.UByteVar import kotlinx.cinterop.refTo -import org.libsodium.* +import org.libsodium.crypto_hash_sha256 +import org.libsodium.crypto_hash_sha256_bytes +import org.libsodium.crypto_hash_sha512 +import org.libsodium.crypto_hash_sha512_bytes +import org.libsodium.crypto_scalarmult +import org.libsodium.crypto_scalarmult_BYTES +import org.libsodium.crypto_scalarmult_base +import org.libsodium.crypto_scalarmult_curve25519_BYTES +import org.libsodium.crypto_sign_BYTES +import org.libsodium.crypto_sign_PUBLICKEYBYTES +import org.libsodium.crypto_sign_SECRETKEYBYTES +import org.libsodium.crypto_sign_detached +import org.libsodium.crypto_sign_ed25519_pk_to_curve25519 +import org.libsodium.crypto_sign_seed_keypair +import org.libsodium.crypto_sign_verify_detached +import org.libsodium.randombytes internal actual object CryptoFactory { - @ExperimentalUnsignedTypes internal actual fun getCrypto(): Crypto = CryptoNativeImpl } -@ExperimentalUnsignedTypes +@OptIn(ExperimentalUnsignedTypes::class) internal object CryptoNativeImpl : CryptoImpl() { override fun sha256(input: ByteArray): ByteArray { @@ -85,6 +99,10 @@ internal object CryptoNativeImpl : CryptoImpl() { return sha512(sharedKey) } + override fun rsaBlind(hm: ByteArray, bks: ByteArray, rsaPubEnc: ByteArray): ByteArray { + TODO("Not yet implemented") + } + private fun ByteArray.toCValuesRef(): CValuesRef<UByteVar> { @Suppress("UNCHECKED_CAST") return this.refTo(0) as CValuesRef<UByteVar> |