TL;DR: RSA public key generated in iOS and stored in the keychain, exported as base64 and sent to a java backend, is not recognized.
I'm implementing a chat encryption feature in an iOS app, and I'm using symmetric + asymmetric keys to handle it.
Without going too much into details, at backend I use the user's public key to encrypt a symmetric key used to encrypt and decrypt messages.
I created two frameworks, respectively in Swift and in Java (backend) to handle key generation, encryption, decryption, etc. I also have tests for them, so I'm 100% everything works as expected.
However, it looks like the backend is unable to recognize the format of the public key passed from iOS. Using RSA both sides, this is the code I use in Swift to generate the key:
// private key parameters
static let privateKeyParams: [String : Any] = [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: "..." // I have a proper unique tag here
]
// public key parameters
static let publicKeyParams: [String : Any] = [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: "..." // I have a proper unique tag here
]
// global parameters for our key generation
static let keyCreationParameters: [String : Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits as String: 2048,
kSecPublicKeyAttrs as String: publicKeyParams,
kSecPrivateKeyAttrs as String: privateKeyParams
]
...
var publicKey, privateKey: SecKey?
let status = SecKeyGeneratePair(Constants.keyCreationParameters as CFDictionary, &publicKey, &privateKey)
I use specular code to read the keys from the keychain.
This is the piece of code I use to export the public key as a base64 string:
extension SecKey {
func asBase64() throws -> String {
var dataPtr: CFTypeRef?
let query: [String:Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: "...", // Same unique tag here
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecReturnData as String: kCFBooleanTrue
]
let result = SecItemCopyMatching(query as CFDictionary, &dataPtr)
switch (result, dataPtr) {
case (errSecSuccess, .some(let data)):
// convert to Base64 string
let base64PublicKey = data.base64EncodedString(options: [])
return base64PublicKey
default:
throw CryptoError.keyConversionError
}
}
}
At backend level I use this Java code to convert the base64 string to a public key:
public PublicKey publicKeyFrom(String data) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] publicBytes = Base64.decodeBase64(data);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
But this fails at the last line, with this exception:
java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException: algid parse error, not a sequence
Doing some manual debugging, I noticed that the format of the public key is different - when I generate a key in iOS and then export as base 64, it looks like this:
MIIBCgKCAQEA4M/bRDdH0f6qFIXxOg13RHka+g4Yv8u9PpPp1IR6pSwrM1aq8B6cyKRwnLe/MOkvODvDfJzvGXGQ01zSTxYWAW1B4uc/NCEemCmZqMosSB/VUJdNxxWtt2hJxpz06hAawqV+6HmweAB2dUn9tDEsQLsNHdwYouOKpyRZGimcF9qRFn1RjR0Q54sUh1tQAj/EwmgY2S2bI5TqtZnZw7X7Waji7wWi6Gz88IkuzLAzB9VBNDeV1cfJFiWsZ/MIixSvhpW3dMNCrJShvBouIG8nS+vykBlbFVRGy3gJr8+OcmIq5vuHVhqrWwHNOs+WR87K/qTFO/CB7MiyiIV1b1x5DQIDAQAB
for a total of 360 characters, whereas doing the same in Java (still using RSA) it's like:
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCAAnWO4BXUGP0qM3Op36YXkWNxb4I2pPZuZ7jJtfUO7v+IO1mq43WzNaxLqqLPkTnMrv2ACRDK55vin+leQlL1z0LzVxjtZ9F6pajQo1r7PqBlL5N8bzBFKpagEf0QfyHPw0/0kG9DMnvQ+Im881QyN2zdl33wp5Fi+jRT7cunFQIDAQAB
with a length of 216 characters.
I'm unable to figure out what's wrong - apparently I wouldn't be surprised if iOS handles keys in a different key, and require special processing in order to talk with other folks.
Any idea?