Main issue
We’re implementing RSA key generation that is required for use with an external API.
The RSA public key needs to be sent to the external API, who will encrypt some text, return it and we can then decrypt it.
We’ve tried implementing this in 3 solutions, two are working however our Swift implementation for iOS is not working (it’s hitting the same API as the other implementation. Copying RSA keypairs that have been generated on the other platforms and hard coding them into Swift works perfectly, so the issue is almost certainly lying with the key generation).
The main question here is; is Apple’s implementation of RSA different and therefore not compatible with the RSA implementation of Javascript and Java? Or is there something obvious that I am missing here?
The provider that we are using are probably not going to do much, as obviously we have it working with two other platforms, so the issue isn’t with their API.
For reference, the provider defines the public key requirement as:
at least a 4096 bit RSA public key, in PKCS1 format, encoded using
Base64 UTF-8 plain encoding (non-mime format).
Code examples
Our swift code:
func generateKeyPair() throws -> (publicKey: SecKey, privateKey: SecKey) {
let attributes: [CFString: Any] = [
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits: 4096,
]
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
throw error!.takeRetainedValue() as Error
}
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecParam), userInfo: nil)
}
return (publicKey: publicKey, privateKey: privateKey)
}
func encodePublicKey(publicKey: SecKey) -> String? {
var error: Unmanaged<CFError>?
guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
print("Error getting external representation of public key")
return nil
}
return publicKeyData.base64EncodedString()
}
We currently have the following code working in Java for an Android app:
public KeyPair generateKeyPair() throws GeneralSecurityException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(4096);
return keyPairGenerator.generateKeyPair();
}
public String encodePublicKey(KeyPair keyPair) throws GeneralSecurityException {
return new String(Base64.getEncoder().encode(keyPair.getPublic().getEncoded()), StandardCharsets.UTF_8))
}
public String decryptAccessToken(KeyPair keyPair, String initialisationVector, String encryptedToken, String encryptedSymmetricKey) throws GeneralSecurityException {
PrivateKey privateKey = keyPair.getPrivate();
Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
OAEPParameterSpec oaepParams = new OAEPParameterSpec("SHA-256", "MGF1", new MGF1ParameterSpec("SHA-256"), PSource.PSpecified.DEFAULT);
rsaCipher.init(Cipher.DECRYPT_MODE, privateKey, oaepParams);
byte[] decryptedSymmetricKey = rsaCipher.doFinal(Base64.getDecoder().decode(encryptedSymmetricKey));
byte[] iv = Base64.getDecoder().decode(initialisationVector.getBytes(StandardCharsets.UTF_8));
SecretKey aesKey = new SecretKeySpec(decryptedSymmetricKey, "AES");
Cipher aesCipher = Cipher.getInstance("AES/GCM/NoPadding");
aesCipher.init(Cipher.DECRYPT_MODE, aesKey, new GCMParameterSpec(128, iv));
byte[] decryptedToken = aesCipher.doFinal(Base64.getDecoder().decode(encryptedToken.getBytes(StandardCharsets.UTF_8)));
return new String(decryptedToken, StandardCharsets.UTF_8);
}
And also the following code working in JavaScript for a web app:
usePublicKey = new Promise((resolve, reject) => {
crypto.subtle.generateKey({
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
}, true, ["encrypt", "decrypt"])
.then((keyPair) => {
crypto.subtle.exportKey("spki", keyPair.publicKey).then((exported) => {
let encodedPublicKey = btoa(String.fromCharCode.apply(null, new Uint8Array(exported)));
resolve(encodedPublicKey);
});
useAccessToken = (serviceResponse, targetCallback) => {
let encryptedSymmetricKey = serviceResponse.encryptedSymmetricKey;
let encodedIv = serviceResponse.initialisationVector;
let encryptedToken = serviceResponse.token;
decryptAccessToken(encryptedToken, encryptedSymmetricKey, encodedIv, keyPair.privateKey).then((decryptedToken) => {
taretCallback(decryptedToken);
})
}
});
});
async function decryptAccessToken(encryptedToken, encryptedSymmetricKey, encodedIv, privateKey) {
let decodedEncryptedSymmetricKey = Uint8Array.from(atob(encryptedSymmetricKey), c => c.charCodeAt(0))
let decryptedSymmetricKey = await crypto.subtle.decrypt({
name: "RSA-OAEP",
hash: {
name: "SHA-256"
}
}, privateKey, decodedEncryptedSymmetricKey);
let aesKey = await crypto.subtle.importKey("raw", decryptedSymmetricKey, "AES-GCM", true, ["encrypt", "decrypt"]);
let decodedIv = Uint8Array.from(atob(encodedIv), c => c.charCodeAt(0));
let decodedEncryptedToken = Uint8Array.from(atob(encryptedToken), c => c.charCodeAt(0))
let rawDecryptedToken = await crypto.subtle.decrypt({
name: "AES-GCM",
iv: decodedIv
}, aesKey, decodedEncryptedToken);
return new TextDecoder('utf-8').decode(new DataView(rawDecryptedToken));
}
Apologies in advance for the long question, I tried to keep it to the point.