I recently had to perform RSA encryption/decryption in Swift for an iOS app. Unfortunately I couldn’t find any examples for how to do this without bringing in a library dependency, which I really wanted to avoid to keep the app efficient and because I can’t reasonably vet their code. And besides: Apple already provides the Security.framework which handles RSA, so it’d be a shame not to use it. It doesn’t bridge nicely to Swift however, so here’s a wrapper I wrote to help use it. I couldn’t find any good examples on the internet so I hope this helps someone:
import Security
// See: https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys
extension SecKey {
enum KeyType {
case rsa
case ellipticCurve
var secAttrKeyTypeValue: CFString {
switch self {
case .rsa:
return kSecAttrKeyTypeRSA
case .ellipticCurve:
return kSecAttrKeyTypeECSECPrimeRandom
}
}
}
/// Creates a random key.
/// Elliptic curve bits options are: 192, 256, 384, or 521.
static func createRandomKey(type: KeyType, bits: Int) throws -> SecKey {
var error: Unmanaged<CFError>?
let keyO = SecKeyCreateRandomKey([
kSecAttrKeyType: type.secAttrKeyTypeValue,
kSecAttrKeySizeInBits: NSNumber(integerLiteral: bits),
] as CFDictionary, &error)
// See here for apple's sample code for memory-managing returned errors
// from the Security framework:
// https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_as_data
if let error = error?.takeRetainedValue() { throw error }
guard let key = keyO else { throw MyErrors.nilKey }
return key
}
/// Gets the public key from a key pair.
func publicKey() throws -> SecKey {
let publicKeyO = SecKeyCopyPublicKey(self)
guard let publicKey = publicKeyO else { throw MyErrors.nilPublicKey }
return publicKey
}
/// Exports a key.
/// RSA keys are returned in PKCS #1 / DER / ASN.1 format.
/// EC keys are returned in ANSI X9.63 format.
func externalRepresentation() throws -> Data {
var error: Unmanaged<CFError>?
let dataO = SecKeyCopyExternalRepresentation(self, &error)
if let error = error?.takeRetainedValue() { throw error }
guard let data = dataO else { throw MyErrors.nilExternalRepresentation }
return data as Data
}
// Self must be the public key returned by publicKey().
// Algorithm should be SecKeyAlgorithm.rsaEncryption* or .eciesEncryption*
func encrypt(algorithm: SecKeyAlgorithm, plaintext: Data) throws -> Data {
var error: Unmanaged<CFError>?
let ciphertextO = SecKeyCreateEncryptedData(self, algorithm,
plaintext as CFData, &error)
if let error = error?.takeRetainedValue() { throw error }
guard let ciphertext = ciphertextO else { throw MyErrors.nilCiphertext }
return ciphertext as Data
}
// Self must be the private/public key pair returned by createRandomKey().
// Algorithm should be SecKeyAlgorithm.rsaEncryption* or .eciesEncryption*
func decrypt(algorithm: SecKeyAlgorithm, ciphertext: Data) throws -> Data {
var error: Unmanaged<CFError>?
let plaintextO = SecKeyCreateDecryptedData(self, algorithm,
ciphertext as CFData, &error)
if let error = error?.takeRetainedValue() { throw error }
guard let plaintext = plaintextO else { throw MyErrors.nilPlaintext }
return plaintext as Data
}
enum MyErrors: Error {
case nilKey
case nilPublicKey
case nilExternalRepresentation
case nilCiphertext
case nilPlaintext
}
}
It boils down to calling:
Here’s some demo code for using the above:
// Elliptic curve test.
do {
let keyPair = try SecKey.createRandomKey(
type: .ellipticCurve,
bits: 384)
let publicKey = try keyPair.publicKey()
let plain = "Chuck Norris has counted to infinity. Twice.".data(using: .utf8)!
let ciphertext = try publicKey.encrypt(
algorithm: .eciesEncryptionStandardVariableIVX963SHA256AESGCM,
plaintext: plain)
let plainAgain = try keyPair.decrypt(
algorithm: .eciesEncryptionStandardVariableIVX963SHA256AESGCM,
ciphertext: ciphertext)
let string = String(data: plainAgain, encoding: .utf8)! // Don't force-unwrap in real code.
print("EC decrypted: " + string)
} catch {
print("Error: \(error)")
}
// RSA test.
do {
let keyPair = try SecKey.createRandomKey(
type: .rsa,
bits: 2048)
let publicKey = try keyPair.publicKey()
let plain = "Chuck Norris can set ants on fire with a magnifying glass. At night.".data(using: .utf8)!
let ciphertext = try publicKey.encrypt(
algorithm: .rsaEncryptionOAEPSHA1AESGCM,
plaintext: plain)
let plainAgain = try keyPair.decrypt(
algorithm: .rsaEncryptionOAEPSHA1AESGCM,
ciphertext: ciphertext)
let string = String(data: plainAgain, encoding: .utf8)! // Don't force-unwrap in real code.
print("RSA decrypted: " + string)
} catch {
print("Error: \(error)")
}
If you’re integrating with an existing system, you’ll simply have to match their key type and algorithm. But if it’s up to you, here’s some tips for how to choose:
Read Apple’s headers! In Xcode, command-click on SecKeyAlgorithm
and read the comments above it. Many algorithms will be noted as legacy, so avoid if possible. And some do not support arbitrary-length data which might be a problem for you. So choose wisely.
And a reasonable option in my opinion is:
I’m not a qualified cryptographer, this information carries no warranty, you should talk to a qualified security professional about the implications of when to use this appropriately!
Thanks for reading, I hope this is helpful, God bless :)
Photo by David Clode on Unsplash
Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.
(Comp Sci, Hons - UTS)
Software Developer (Freelancer / Contractor) in Australia.
I have worked at places such as Google, Cochlear, Assembly Payments, News Corp, Fox Sports, NineMSN, FetchTV, Coles, Woolworths, Trust Bank, and Westpac, among others. If you're looking for help developing an iOS app, drop me a line!
Get in touch:
[email protected]
github.com/chrishulbert
linkedin