-1

I'm trying to create an RSAPublicKeySpec with the JWK modulus and exponent provided by CyberSource. The goal is to use it to encrypt a given credit card number. I'm stuck with this error "RSA Exponent is even"

Example JWK:

{
"kty": "RSA",
"use": "enc",
"kid": "05BgbFie7vX5vzSMKOoqEAAdfpdR4kas",
"n": "fC1G6rVue4w_jjJrKPZusGN9G-Y7mWuLJ0O2_GHd94LvR51-ok7ahuQUVMZLdigixnspaGo_WVLvTTZ5J28Cc1uSx0o_BsxpNaAQD8_aBZL3nnAiBLACxI1JHAVo7SXAJQmz-mqVFYTppg9QmpB2ATTmXjUOQy-Fqkw3EByfCANHhHSNs4-HASovsfcRMUmmvDfTd5qBb23KzDJeDVqTYWa3XjUorlZOCJuLyPgeDEK8oOC9C4W9dn32z8FJ4E6Dz28M_2O3g8FLQD2F-NezkQJsl8MEYo4rl1nr7_oIkMsYLCCoG8jwmryErb7km9JWWgqZ80trkjijFqDAbHfUEw",
"e": "AQAB"
} 

Android Kotlin code:

// Here I pass a CC number like "2134345613458954" and n (modulus) e (exponent) from JWK above
@RequiresApi(Build.VERSION_CODES.O)
fun generateRSAEncryption(cardNumber: String, modulus: String, exponent: String): String {
    val modules = BigInteger(1, modulus.toByteArray())
    val exponent = BigInteger(1, exponent.toByteArray())
    val pubSpec = RSAPublicKeySpec(modules, exponent)
    val publicKey = KeyFactory.getInstance("RSA").generatePublic(pubSpec)
    val oaepFromAlgo = Cipher.getInstance("RSA/None/OAEPWithSHA256AndMGF1Padding", "BC")
    oaepFromAlgo.init(Cipher.ENCRYPT_MODE, publicKey) // error here
    val cipherText = oaepFromAlgo.doFinal(Base64.getEncoder().encode(cardNumber.toByteArray()))
    return Base64.getEncoder().encodeToString(cipherText) // pass this to Flex
}

Example of expected result:

"LFDh8upgXaiUX0iNaCOcHgeaotRCNsDpdJf5SqACpiL38JVnpHW7bb/g3yM67uett1tUSPG9o1yexNaAneur4P2jbpSnU0kWWK7NpLIQWAvjmCVxGWceZdFPGvB+E2hQncvIImlYo+d/XIHZOUonVmDoj+pKouxmd60lpaMTrq7sJ8BrfWCDG1lJJ0M2S98CoDb19xK+XCn+cpd3KkTHsGJGHA6inT2stHxYJrF7dd3r1xuH0WW1gpRnRaXwl6BFZW9EzCCzaWZmifZYIPFXZIE44pU9xRCfjD1IUKXKLxw0l6cFAlaP0SHG2t9HDDMLjNQjvqRarFiPoAjtwfW7Zw=="
Marc Nunes
  • 238
  • 3
  • 13

2 Answers2

1

Modulus and exponent in the JWK are Base64url encoded and therefore must be Base64url decoded:

...
val modulusBytes = Base64.getUrlDecoder().decode(modulus)
val exponentBytes = Base64.getUrlDecoder().decode(exponent)
val modules = BigInteger(1, modulusBytes)
val exponent = BigInteger(1, exponentBytes)
...

However your key is classified as weak by BC and throws an exception: RSA modulus has a small prime factor.

This vulnerability is identified by this BC logic.


If the key is to be used anyway: BouncyCastle allows disabling the check with

System.getProperties().put("org.bouncycastle.rsa.allow_unsafe_mod", "true")

However, this does not work on my machine under Kotlin with the BC Provider installed by default. You have to uninstall the old BC Provider and install a newer version:

Security.removeProvider("BC")
Security.addProvider(BouncyCastleProvider())
System.getProperties().put("org.bouncycastle.rsa.allow_unsafe_mod", "true")

with the corresponding BC entry in the gradle file under dependencies, e.g.

implementation 'org.bouncycastle:bcprov-jdk18on:1.71' 

This way the check can be disabled on my machine so that the exception is not thrown.

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • interesting. Ya, I think so at least following their example this is how it's done. I was able to get it working with JS (https://developer.cybersource.com/library/documentation/dev_guides/Secure_Acceptance_Flex/Key/html/wwhelp/wwhimpl/js/html/wwhelp.htm#href=keys.html). Still not working on Android though. With your help, it generates the key now but Cybersource doesn't like it. I get a 400 "Cannot decrypt PAN (RsaOaep): unable to decrypt block"" – Marc Nunes Aug 03 '22 at 21:02
  • @MarcNunes: I think you're missing an important hint. The fact that the modulus `n` has small prime factors means it's like incorrect, garbled, invalid. – President James K. Polk Aug 03 '22 at 21:19
  • @MarcNunes - There are three differences between both codes: The Kotlin code does Base64 encode the plaintext before encryption, while the JS code does not. Since a Base64 encoding of the plaintext before encryption is pointless, the Kotlin code should be fixed. The 2nd difference is that the Kotlin code Base64 encodes the ciphertext, while the JS code does not. The 3rd difference is that BC/Kotlin declines the posted weak key by default, while JS does not. Apart from that, both codes are identical (including the OAEP parameters: SHA256 for both digests, OAEP and MGF1). – Topaco Aug 04 '22 at 05:36
  • @Topaco I understand. I'm still looking into this still but it does sound like we might need to take a different approach for mobile. I would like to look into this further and will make sure I update this thread either way. – Marc Nunes Aug 08 '22 at 20:11
0

Just in case someone finds this useful. It turns out our servers were stripping out the public key (Node JS uses the JWK only). With the public this is a much easier way to encrypt CC on mobile. Here is an example of the full payload from Cybersource:

{
  "keyId": "05BgbFie7vX5vzSMKOoqEAAdfpdR4kas",
  "der": {
    "format": "X.509",
    "algorithm": "RSA",
    "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgC1G6rVue4w/jjJrKPZusGN9G+Y7mWuLJ0O2/GHd94LvR51+ok7ahuQUVMZLdigixnspaGo/WVLvTTZ5J28Cc1uSx0o/BsxpNaAQD8/aBZL3nnAiBLACxI1JHAVo7SXbJQmz+mqVFYTppg9QmpB2ATTmXjUOQy+Fqkw3EByfCANHhHSNs4+HASovsfcRMUmmvDfTd5qBb23KzDJeDVqTYWa3XjUorlZOCJuLyPgeDEK8oOC9C4W9dn32z8FJ4E6Dz28M/2O3g8FLQD2F+NezkQJsl8MEYo4rl1nr7/oIkMsYLCCoG8jwmryErb7km9JWWgqZ80trkjijFqDAbHfUEwIDAQAB"
  },
  "jwk": {
    "kty": "RSA",
    "use": "enc",
    "kid": "05BgbFie7vX5vzSMKOoqEAAdfpdR4kas",
    "n": "fC1G6rVue4w_jjJrKPZusGN9G-Y7mWuLJ0O2_GHd94LvR51-ok7ahuQUVMZLdigixnspaGo_WVLvTTZ5J28Cc1uSx0o_BsxpNaAQD8_aBZL3nnAiBLACxI1JHAVo7SXAJQmz-mqVFYTppg9QmpB2ATTmXjUOQy-Fqkw3EByfCANHhHSNs4-HASovsfcRMUmmvDfTd5qBb23KzDJeDVqTYWa3XjUorlZOCJuLyPgeDEK8oOC9C4W9dn32z8FJ4E6Dz28M_2O3g8FLQD2F-NezkQJsl8MEYo4rl1nr7_oIkMsYLCCoG8jwmryErb7km9JWWgqZ80trkjijFqDAbHfUEw",
    "e": "AQAB"
  }
}

And you can handle this like:

@RequiresApi(Build.VERSION_CODES.O)
fun generateRSAEncryption(cardNumber: String, key: String): String { //public key comes for DER component
    val decodedKey = Base64.getDecoder().decode(key)
    val keySpec = X509EncodedKeySpec(decodedKey)
    val publicKey = KeyFactory.getInstance("RSA").generatePublic(keySpec)
    val oaepFromAlgo = Cipher.getInstance("RSA/None/OAEPwithSHA-1andMGF1Padding", "BC")
    oaepFromAlgo.init(Cipher.ENCRYPT_MODE, publicKey)
    val cipherText = oaepFromAlgo.doFinal(cardNumber.toByteArray())
    return Base64.getEncoder().encodeToString(cipherText) // pass this to Flex
}
Marc Nunes
  • 238
  • 3
  • 13