0

I have what seems to be a simple need - I want to generate a public key that I can put into an app, and use to to encrypt data that may be exposed to others. Later, using a private key known only to myself, I want to be able to decrypt that data.

There are bits and pieces of the solution to this scattered all over, but I have yet to find a good explanation of how to:

  • generate keys that can be turned into string representations that then can be used to reconstruct the keys
  • pass Data objects into the encryption, and get a string representation of the encrypted data
  • turn the string representing the encrypted data back into a Data object, and then decrypt this data into its original form
  • do all the above only using Swift 4.1 or newer

I am aware that there are frameworks that can do this, but it seems like this should ultimately be a fairly small piece of code and so a framework is overkill.

David H
  • 40,852
  • 12
  • 92
  • 138
  • I thought I had a workable solution to this, but when I threw large chunks of data at it it failed. RSA doesn't work for asymmetric and large data sets. – David H Dec 17 '18 at 01:55
  • 1
    Usually this would be considered a simple need, I agree. Introducing Swift and CommonCrypto into the picture make this a far more complex scenario. Apple's support for modern cryptography needs is... well non-existent. Biting the bullet and using a library is probably your best bet. – Luke Joshua Park Dec 17 '18 at 04:49
  • 2
    Dou you remember [Hybrid Cryptosystem](https://en.wikipedia.org/wiki/Hybrid_cryptosystem). The RSA will be much slower compared to block ciphers. You can use [DHKE](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange) generate the ephemeral keys and use in symmetric encryption. – kelalaka Dec 17 '18 at 06:44
  • 1
    There’s a very good wrapper called “RNcryptor” https://github.com/RNCryptor/RNCryptor which can help save lot of time – Sachin Raut Dec 17 '18 at 07:31

1 Answers1

0

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
}

}


David H
  • 40,852
  • 12
  • 92
  • 138