swiftsymmetric-keycryptokit

How to generate SymmetricKey from password - CryptoKit


I have the following code:

public extension SymmetricKey {
    init?(base64Encoded base64EncodedKeyString: String) {
        guard let keyData = Data(base64Encoded: base64EncodedKeyString) else { return nil }
        self.init(data: keyData)
    }
    
    var base64: String {
        self.withUnsafeBytes { body in
            Data(body).base64EncodedString()
        }
    }
    
    init?(pass: String) {
        guard let passHash = passToHash(pass: pass) else { return nil }
        
        self.init(base64Encoded: passHash)
    }
}

fileprivate func passToHash(pass: String) -> String? {
    guard let passData = pass.data(using: .utf8) else { return nil }
    let hashed = SHA256.hash(data: passData)
    return hashed.compactMap { String(format: "%02x", $0) }.joined()
}

problem is if I will try to use SymmetricKey I will see error:

The operation couldn’t be completed. (CryptoKit.CryptoKitError error 1.)

This error means incorrect key size.

If I will try to compare SymmetricKeys generated with default way and from my custom init I will see difference:

5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5 //from password
cfyk1v2dRIlVKHlpy8qBH71v4BXGSUHPUS3wLn1gzj0= //.bits256

enter image description here

What I'm doing wrong? Why hashes looks different?


Solution

  • First, this is a very insecure key-derivation function. No serious encryption scheme should use this. For creating a key from a password, you must use a PBKDF (password-based key-derivation function) such as PBKDF2.

    For compatibility purposes, you may need to implement this broken scheme, so I'll dive into that here.

    Keys are fundamentally Data. You've created a hex-encoded String, and then Base64-encoded that. This makes the resulting Data twice as long as it should be. What you meant was this:

    public extension SymmetricKey {
        var base64: String {
            self.withUnsafeBytes { body in
                Data(body).base64EncodedString()
            }
        }
    
        // Never do this. This is incredibly insecure
        init(pass: String) {
            // This is a completely broken KDF.
            let hash = SHA256.hash(data: Data(pass.utf8)).withUnsafeBytes { ptr in
                Data(ptr)
            }
            self.init(data: hash)
        }
    }
    
    let key1 = SymmetricKey(size: .bits256).base64
    // ZHpD7LwSwhOT0msWf0XOJItqUezMSLwdxcLloSMSMHs
    let key2 = SymmetricKey(pass: "12345").base64
    // WZRHGrsBESr8wYFZ9sx0tPURuZgG2lmzyvWpwXPKz8U
    
    // key2 is very easy to brute-force compared to proper AES-256
    

    Unfortunately, Apple's CryptoKit does not include any proper PBKDF. It is a very limited framework that only provides primitives for some very specific use cases. Some popular frameworks that provide the tools you need are CryptoSwift and IDZSwiftCommonCrypto. The more general approach is to use Apple's CommonCryptor, but it's not particularly fun from Swift.

    Here is an example, however.

    public extension SymmetricKey {
        var base64: String {
            self.withUnsafeBytes { body in
                Data(body).base64EncodedString()
            }
        }
    
        init(password: String, salt: Data) {
            self.init(data: Self.makeKey(forPassword: password, withSalt: salt))
        }
    
        private static func makeKey(forPassword password: String, withSalt salt: Data) -> Data {
    
            let passwordArray = password.utf8.map(Int8.init)
            let saltArray = Array(salt)
            let keySize = 32
    
            var derivedKey = Array<UInt8>(repeating: 0, count: keySize)
    
            // All the crazy casting because CommonCryptor hates Swift
            let algorithm    = CCPBKDFAlgorithm(kCCPBKDF2)
            let prf          = CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA1)
            let pbkdf2Rounds = UInt32(10000)
    
            let result = CCCryptorStatus(
                CCKeyDerivationPBKDF(
                    algorithm,
                    passwordArray, passwordArray.count,
                    saltArray,     saltArray.count,
                    prf,           pbkdf2Rounds,
                    &derivedKey,   keySize)
            )
            guard result == CCCryptorStatus(kCCSuccess) else {
                fatalError("SECURITY FAILURE: Could not derive secure password (\(result))")
            }
            return Data(derivedKey)
        }
    }
    

    Calling this requires passing a salt. How to choose the salt depends a bit on your use case. A common situation is to use a random salt (say, 16 bytes) for every encryption, and then pass the salt along with the encrypted data so that the receiver can decrypt it. Here's a way to make that kind of random data:

    func makeSalt(length: Int) -> Data {
        var data = Data(count: length)
        data.withUnsafeMutableBytes {
            SecRandomCopyBytes(kSecRandomDefault, length, $0.baseAddress!)
        }
        return data
    }
    

    You can also get functions that do these things in the aforementioned frameworks. Very little about doing cryptography in Swift is fun.