Search code examples
swiftencryptionapple-cryptokit

Swift CryptoKit encryption yields inconsistent results


I have a simple encryption routine that produces different results each time it is executed. Can anyone identify the problem? The code below just shows the same call three different times and as you can see the results are different on each call.

import SwiftUI
import CryptoKit

struct ContentView: View {
    @State var password = "TopSecret"
    @State var text = "My text"
    
    var body: some View {
        VStack {
            TextField("Password", text: $password)
            TextField("Text", text: $text)
            Text(try! encrypt(password: password, text: text).hexEncodedString())
            Text(try! encrypt(password: password, text: text).hexEncodedString()).foregroundColor(Color.blue)
            Text(try! encrypt(password: password, text: text).hexEncodedString())
        }
    }
}

func encrypt(password: String, text: String) throws -> Data {
    let x = [UInt8](text.utf8)
    return try encrypt(password: password, input: x)
}

func encrypt(password: String, input: [UInt8]) throws -> Data {
    let pwd = password.padding(toLength: 32, withPad: "@", startingAt: 0)
    let key = SymmetricKey(data: pwd.data(using: String.Encoding.utf8)!)
    let sealedBox = try AES.GCM.seal(input, using: key)
    return sealedBox.combined!
}

extension Data {
    struct HexEncodingOptions: OptionSet {
        let rawValue: Int
        static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
    }
    func hexEncodedString(options: HexEncodingOptions = []) -> String {
        let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
        return self.map { String(format: format, $0) }.joined()
    }
}

Solution

  • This is a feature of secure encryption schemes such as the seal method. There is a random value (called the "nonce") included to ensure that every encryption is unique. All of these will decrypt properly.

    If you require consiste nt (insecure) encryption results, for example in unit testing you can pass a static nonce value by passing a nonce parameter to seal.

    Note that your key derivation is extremely insecure. You can't just pad an ASCII string into an AES key. It destroys the keyspace. For a computer-generated key, you should generate a random 32 bytes. For a human-generated password, you need a PBKDF (such as PBKDF2 or scrypt) to stretch the key. Unfortunately, I don't believe CryptoKit provides any tools to support password-based encryption; only random key encryption. You'll need to perform key stretching by hand using something like CommonCrypto or CryptoSwift, or you'll need to use a tool that supports passwords like Themis or RNCryptor.