Inspiration for this answer comes from Swift 3 export SecKey to String and NSString Crypt.m gist.
Don't know what PKI (Public Key Encryption)is? Then a nice tutorial: Everything you should know about certificates and PKI but are too afraid to ask
The recommended way to use public/private encryption for large data sets is to use public/private encryption to first share a temporary symmetric key, then use that key to encrypt, send the data, and allow the remote side to decrypt it.
That seemed like a complex task, but then it turned out that Apple already provides this capability for iOS/macOS (search for "In fact, the certificate, key, and trust services API provides a simple way to accomplish this.")
The code below is designed as a test vehicle to let anyone experiment with the settings. You exercise it twice - one with the #if
set to true
, the second to false
. In the first run (using the Simulator for sure), you get string representations of the public and private keys. You then paste those into the class properties, change the #if
setting, and re-run the test
method.
The test
method then re-creates both keys, encrypts the supplied data, then hands that encrypted data off to be decrypted. In the end, the original data is compared to the decrypted data and the result printed out. The raw code is available as gist on github.
The code is constructed as a test vehicle - you can run it as is to have it produce two strings representing the private and public key, then paste those in below to verify that the strings themselves perform exactly as a test with keys freshly generated. The test has four stages:
- run an encrypt and decrypt with just the generated keys (SecKeys)
- convert the public key to a string, then recreate a new public SecKey from the string, then do the encrypt/decrypt test
- same as above except convert private key to string and back
- same as above but both keys converted to strings and back
The gist above also contains a class Asymmetric
that just uses the public key to encrypt data - this is what you would use in an app (but it's completely based on methods in this class).
You use either a RSA algorithm or an Elliptic Curve key pair. With the current key size, both use a AES 128-bit symmetric key for actual data encryption (see Apple headers).
#if true
private let keyType = kSecAttrKeyTypeRSA // kSecAttrKeyTypeEC
private let algorithm = SecKeyAlgorithm.rsaEncryptionOAEPSHA512AESGCM // EncryptionOAEPSHA512AESGCM
private let keySize = 4096 // SecKey.h states that with this size, you get AES 256-bit encoding of the payload
#else
private let keyType = kSecAttrKeyTypeECSECPrimeRandom // kSecAttrKeyTypeECSECPrimeRandom
private let algorithm = SecKeyAlgorithm.eciesEncryptionCofactorVariableIVX963SHA512AESGCM
private let keySize = 384 // SecKey.h states that with this size, you get AES 256-bit encoding of the payload
#endif
@objcMembers
final class AsymmetricTest: NSObject {
This is where you will paste in the keys you generate when running the program.
// Some Key pair I generated - replace with your own
private let publicStr = """
BFZjQQZVrcHitn13Af89ASrRT2VVPa4yGCreBJim52R/d3yJj3iTroanc7XW+YLJpijFBMei6ddf
lb2PjJLvXNJy8hQItCFRlpbGj7ddSCOuBNyjQP+cpmddgFhy8KCbgw==
"""
private let privateStr = """
BFZjQQZVrcHitn13Af89ASrRT2VVPa4yGCreBJim52R/d3yJj3iTroanc7XW+YLJpijFBMei6ddf
lb2PjJLvXNJy8hQItCFRlpbGj7ddSCOuBNyjQP+cpmddgFhy8KCbg+Sy8M4IjGDI5gdzNmWhDQp2
mggdySIqrjVobCL5NcAg5utA/2QdJGCy9mPw0GkFHg==
"""
var publicKey: Data = Data()
var privateKey: Data = Data()
Run the 4 tests. You provide the data - I tested with a few thousand bytes, but it should work for any data size.
func test(_ testData: Data) -> Bool {
func key2string(key: SecKey) -> String {
guard let keyData = secKey2data(key: key) else { fatalError("key2string FAILED!!!") }
let base64publicKey = keyData.base64EncodedString(options: [.lineLength76Characters, .endLineWithCarriageReturn])
return base64publicKey
}
func string2key(str: String, cfType: CFString) -> SecKey? {
let d = Data(base64Encoded: str, options: [.ignoreUnknownCharacters])
print("string2key: dataSize =", d?.count ?? "-1")
guard
let data = Data(base64Encoded: str, options: [.ignoreUnknownCharacters]),
let key = data2secKey(keyData: data, cfType: cfType)
else { return nil }
return key
}
func runTest(data testData: Data, keys: (public: SecKey, private: SecKey)) {
let d1 = Date()
let _ = self.encryptData(data: testData, key: keys.public)
print("Time:", -d1.timeIntervalSinceNow) // measure performance
if
let d1 = self.encryptData(data: testData, key: keys.public)
,
let d2 = self.decryptData(data: d1, key: keys.private)
{
print("Input len:", d1.count, "outputLen:", d2.count)
print("Reconstructed data is the same as input data:", testData == d2 ? "YES" : "NO")
} else {
print("TEST FAILED")
}
}
If you set the line below to false
, then instead of generating keys, it will use the two strings at the top of the class.
#if true // set to true, then copy the two strings to publicStr and privateStr above and set this to false
guard let keys = createKey(keySize: keySize) else { print("WTF"); return false } // size is important smaller failed for me
print("PUBLIC:\n\(key2string(key: keys.public))\n")
print("PRIVATE:\n\(key2string(key: keys.private))\n")
runTest(data: testData, keys: keys) // Original Keys
do { // So suppose we have our public app - it gets the public key in base64 format
let base64key = key2string(key: keys.public)
guard let key = string2key(str: base64key, cfType: kSecAttrKeyClassPublic) else { fatalError("FAILED!") }
runTest(data: testData, keys: (key, keys.private)) // Reconstructed public
}
do { // So suppose we have our private app - it gets the private key in base64 format
let base64key = key2string(key: keys.private)
guard let key = string2key(str: base64key, cfType: kSecAttrKeyClassPrivate) else { fatalError("FAILED!") }
runTest(data: testData, keys: (keys.public, key)) // Reconstructed private
}
do {
let base64keyPublic = key2string(key: keys.public)
guard let keyPublic = string2key(str: base64keyPublic, cfType: kSecAttrKeyClassPublic) else { fatalError("FAILED!") }
let base64keyPrivate = key2string(key: keys.private)
guard let keyPrivate = string2key(str: base64keyPrivate, cfType: kSecAttrKeyClassPrivate) else { fatalError("FAILED!") }
runTest(data: testData, keys: (keyPublic, keyPrivate)) // Reconstructed private
}
#else
do {
guard let keyPublic = string2key(str: publicStr, cfType: kSecAttrKeyClassPublic) else { fatalError("FAILED!") }
guard let keyPrivate = string2key(str: privateStr, cfType: kSecAttrKeyClassPrivate) else { fatalError("FAILED!") }
runTest(data: testData, keys: (keyPublic, keyPrivate)) // Reconstructed private
}
#endif
return true
}
Encrypts the supplied data with the supplied key (which should be the public key):
func encryptData(data: Data, key: SecKey) -> Data? {
//var status: OSStatus = noErr
var error: Unmanaged<CFError>?
let cfData: CFData = data as NSData as CFData
guard SecKeyIsAlgorithmSupported(key, .encrypt, algorithm) else {
fatalError("Can't use this algorithm with this key!")
}
if let encryptedCFData = SecKeyCreateEncryptedData(key, algorithm, cfData, &error) {
return encryptedCFData as NSData as Data
}
if let err: Error = error?.takeRetainedValue() {
print("encryptData error \(err.localizedDescription)")
}
return nil
}
Decrypts the supplied data with the supplied key (which should be the private key):
func decryptData(data: Data, key: SecKey) -> Data? {
var error: Unmanaged<CFError>?
let cfData: CFData = data as NSData as CFData
guard SecKeyIsAlgorithmSupported(key, .decrypt, algorithm) else {
fatalError("Can't use this algorithm with this key!")
}
if let decryptedCFData = SecKeyCreateDecryptedData(key, algorithm, cfData, &error) {
return decryptedCFData as NSData as Data
} else {
if let err: Error = error?.takeRetainedValue() {
print("Error \(err.localizedDescription)")
}
return nil
}
}
Genreate a key - you should only need to do this one in a real world situation, then make sure the private key stays private:
func createKey(keySize: Int) -> (public: SecKey, private: SecKey)? {
var sanityCheck: OSStatus = 0
let publicKeyAttr:[CFString: Any] = [
kSecAttrIsPermanent : 0,
kSecAttrApplicationTag : "com.asymmetric.publickey".data(using: .ascii)!
]
let privateKeyAttr:[CFString: Any] = [
kSecAttrIsPermanent : 0,
kSecAttrApplicationTag : "com.asymmetric.privatekey".data(using: .ascii)!
]
let keyPairAttr:[CFString: Any] = [
kSecAttrKeyType : keyType,
kSecAttrKeySizeInBits : keySize,
kSecPrivateKeyAttrs : privateKeyAttr,
kSecPublicKeyAttrs : publicKeyAttr
]
var publicKey: SecKey? = nil
var privateKey: SecKey? = nil
sanityCheck = SecKeyGeneratePair(keyPairAttr as CFDictionary, &publicKey, &privateKey)
if sanityCheck == noErr {
return (publicKey!, privateKey!)
} else {
print("Fucked!")
return nil
}
}
Method that converts a SecKey
to Data
:
func secKey2data(key: SecKey) -> Data? {
var error:Unmanaged<CFError>?
guard let keyData = SecKeyCopyExternalRepresentation(key, &error) as Data? else { error?.release(); return nil }
//print("secKey2data size \(keyData.count)")
return keyData
}
Method that converts Data
to a SecKey
:
func data2secKey(keyData: Data, cfType: CFString) -> SecKey? {
var error:Unmanaged<CFError>?
let attrs: [CFString: Any] = [
kSecAttrKeyType: keyType,
kSecAttrKeyClass: cfType
]
let key = SecKeyCreateWithData(keyData as CFData, attrs as CFDictionary, &error)
if let err: Error = error?.takeRetainedValue() {
//let nsError: NSError = realErr
print("data2secKey ERR: \(err.localizedDescription)")
}
return key
}
}