aescommoncryptoswift5

Swift 5 + kCCDecrypt (CommonCrypto): Failing to decrypt


Trying to write my own encrypt/decrypt functions in Swift 5, based on tons of other similar questions -- and failing miserably.

I'm using CommonCrypto + CCCrypt to encrypt/decrypt (AES, 256 key, random iv).

I'm favouring NSData.bytes over withUnsafeBytes (which is just too confusing in Swift 5).

My encrypt function goes like this:

func encrypt(_ string: String) throws -> Data {
    guard let dataToEncrypt: Data = string.data(using: .utf8) else {
        throw AESError.stringToDataFailed
    }

    // Seems like the easiest way to avoid the `withUnsafeBytes` mess is to use NSData.bytes.
    let dataToEncryptNSData = NSData(data: dataToEncrypt)

    let bufferSize: Int = ivSize + dataToEncryptNSData.length + kCCBlockSizeAES128
    let buffer = UnsafeMutablePointer<NSData>.allocate(capacity: bufferSize)
    defer { buffer.deallocate() }

    let status: Int32 = SecRandomCopyBytes(
        kSecRandomDefault,
        kCCBlockSizeAES128,
        buffer
    )
    guard status == 0 else {
        throw AESError.generateRandomIVFailed
    }

    var numberBytesEncrypted: Int = 0

    let cryptStatus: CCCryptorStatus = CCCrypt( // Stateless, one-shot encrypt operation
        CCOperation(kCCEncrypt),                // op: CCOperation
        CCAlgorithm(kCCAlgorithmAES),           // alg: CCAlgorithm
        options,                                // options: CCOptions
        key.bytes,                              // key: the "password"
        key.length,                             // keyLength: the "password" size
        buffer,                                 // iv: Initialization Vector
        dataToEncryptNSData.bytes,              // dataIn: Data to encrypt bytes
        dataToEncryptNSData.length,             // dataInLength: Data to encrypt size
        buffer + kCCBlockSizeAES128,            // dataOut: encrypted Data buffer
        bufferSize,                             // dataOutAvailable: encrypted Data buffer size
        &numberBytesEncrypted                   // dataOutMoved: the number of bytes written
    )

    guard cryptStatus == CCCryptorStatus(kCCSuccess) else {
        throw AESError.encryptDataFailed
    }

    return Data(bytes: buffer, count: numberBytesEncrypted + ivSize)
}

The decrypt function:

func decrypt(_ data: Data) throws -> String {

    // Seems like the easiest way to avoid the `withUnsafeBytes` mess is to use NSData.bytes.
    let dataToDecryptNSData = NSData(data: data)

    let bufferSize: Int = dataToDecryptNSData.length - ivSize
    let buffer = UnsafeMutablePointer<NSData>.allocate(capacity: bufferSize)
    defer { buffer.deallocate() }

    var numberBytesDecrypted: Int = 0

    let cryptStatus: CCCryptorStatus = CCCrypt(         // Stateless, one-shot encrypt operation
        CCOperation(kCCDecrypt),                        // op: CCOperation
        CCAlgorithm(kCCAlgorithmAES128),                // alg: CCAlgorithm
        options,                                        // options: CCOptions
        key.bytes,                                      // key: the "password"
        key.length,                                     // keyLength: the "password" size
        dataToDecryptNSData.bytes,                      // iv: Initialization Vector
        dataToDecryptNSData.bytes + kCCBlockSizeAES128, // dataIn: Data to decrypt bytes
        bufferSize,                                     // dataInLength: Data to decrypt size
        buffer,                                         // dataOut: decrypted Data buffer
        bufferSize,                                     // dataOutAvailable: decrypted Data buffer size
        &numberBytesDecrypted                           // dataOutMoved: the number of bytes written
    )

    guard cryptStatus == CCCryptorStatus(kCCSuccess) else {
        throw AESError.decryptDataFailed
    }

    let decryptedData = Data(bytes: buffer, count: numberBytesDecrypted)

    guard let decryptedString = String(data: decryptedData, encoding: .utf8) else {
        throw AESError.dataToStringFailed
    }

    return decryptedString
}

Those were based on this awesome answer from the user "@zaph".

Although encrypt seems to be working, decrypt fails.

This line specifically:

guard let decryptedString = String(data: decryptedData, encoding: .utf8) else {
    throw AESError.dataToStringFailed
}

So of course I'm missing something, but I can't figure out what it is. Could you?

Here is the pastebin with the entire code, which you could copy/paste in a Playground and hit play. Swift 5 is required: https://pastebin.com/raw/h6gacaHX

Update
I'm now following @OOper's suggested approach. The final code can be seen here:
https://github.com/backslash-f/aescryptable


Solution

  • In fact, using Data.withUnsafeBytes is sort of a mess in Swift 5, though NSData.bytes cannot be the easiest way, as using it would sometimes seemingly work, and sometimes not.

    You need to be accustomed work with Data.withUnsafeBytes if you want to write an always-works code in Swift with Data.

    struct AES {
        private let key: Data //<- Use `Data` instead of `NSData`
    
        private let ivSize: Int                     = kCCBlockSizeAES128
        private let options: CCOptions              = CCOptions(kCCOptionPKCS7Padding)
    
        init(keyString: String) throws {
            guard keyString.count == kCCKeySizeAES256 else {
                throw AESError.invalidKeySize
            }
            guard let keyData: Data = keyString.data(using: .utf8) else {
                throw AESError.stringToDataFailed
            }
            self.key = keyData
        }
    }
    
    extension AES: Cryptable {
    
        func encrypt(_ string: String) throws -> Data {
            guard let dataToEncrypt: Data = string.data(using: .utf8) else {
                throw AESError.stringToDataFailed
            }
    
            let bufferSize: Int = ivSize + dataToEncrypt.count + kCCBlockSizeAES128
            var buffer = Data(count: bufferSize)
    
            let status: Int32 = buffer.withUnsafeMutableBytes {bytes in
                SecRandomCopyBytes(
                    kSecRandomDefault,
                    kCCBlockSizeAES128,
                    bytes.baseAddress!
                )
            }
            guard status == 0 else {
                throw AESError.generateRandomIVFailed
            }
    
            var numberBytesEncrypted: Int = 0
    
            let cryptStatus: CCCryptorStatus = key.withUnsafeBytes {keyBytes in
                dataToEncrypt.withUnsafeBytes {dataBytes in
                    buffer.withUnsafeMutableBytes {bufferBytes in
                        CCCrypt( // Stateless, one-shot encrypt operation
                            CCOperation(kCCEncrypt),                // op: CCOperation
                            CCAlgorithm(kCCAlgorithmAES),           // alg: CCAlgorithm
                            options,                                // options: CCOptions
                            keyBytes.baseAddress,                   // key: the "password"
                            key.count,                              // keyLength: the "password" size
                            bufferBytes.baseAddress,                // iv: Initialization Vector
                            dataBytes.baseAddress,                  // dataIn: Data to encrypt bytes
                            dataToEncrypt.count,                    // dataInLength: Data to encrypt size
                            bufferBytes.baseAddress! + kCCBlockSizeAES128, // dataOut: encrypted Data buffer
                            bufferSize,                             // dataOutAvailable: encrypted Data buffer size
                            &numberBytesEncrypted                   // dataOutMoved: the number of bytes written
                        )
                    }
                }
            }
    
            guard cryptStatus == CCCryptorStatus(kCCSuccess) else {
                throw AESError.encryptDataFailed
            }
    
            return buffer[..<(numberBytesEncrypted + ivSize)]
        }
    
        func decrypt(_ data: Data) throws -> String {
    
            let bufferSize: Int = data.count - ivSize
            var buffer = Data(count: bufferSize)
    
            var numberBytesDecrypted: Int = 0
    
            let cryptStatus: CCCryptorStatus = key.withUnsafeBytes {keyBytes in
                data.withUnsafeBytes {dataBytes in
                    buffer.withUnsafeMutableBytes {bufferBytes in
                        CCCrypt(         // Stateless, one-shot encrypt operation
                            CCOperation(kCCDecrypt),                        // op: CCOperation
                            CCAlgorithm(kCCAlgorithmAES128),                // alg: CCAlgorithm
                            options,                                        // options: CCOptions
                            keyBytes.baseAddress,                           // key: the "password"
                            key.count,                                      // keyLength: the "password" size
                            dataBytes.baseAddress,                          // iv: Initialization Vector
                            dataBytes.baseAddress! + kCCBlockSizeAES128,    // dataIn: Data to decrypt bytes
                            bufferSize,                                     // dataInLength: Data to decrypt size
                            bufferBytes.baseAddress,                        // dataOut: decrypted Data buffer
                            bufferSize,                                     // dataOutAvailable: decrypted Data buffer size
                            &numberBytesDecrypted                           // dataOutMoved: the number of bytes written
                        )
                    }
                }
            }
    
            guard cryptStatus == CCCryptorStatus(kCCSuccess) else {
                throw AESError.decryptDataFailed
            }
    
            let decryptedData = buffer[..<numberBytesDecrypted]
    
            guard let decryptedString = String(data: decryptedData, encoding: .utf8) else {
                throw AESError.dataToStringFailed
            }
    
            return decryptedString
        }
    
    }