CryptoSwift - Getting nil when using makeEncryptor() for string of less than 16 bytes in iOS

627 Views Asked by At

I am using cryptoSwift library for encryption and decryption. But it's working with only string 16 bytes. If i am passing small string or less than 16 bytes then getting nil result.

I am using Incremental operations use instance of Cryptor and encrypt/decrypt one part at a time.

Please help me here, is there anything which i am doing wrong.

Thanks in advance.

func encAndDec(){
        do {
            // Encryption start
            let data =  Data.init(base64Encoded: "12345678901234567890123456789012".base64Encoded()!)
            let iv : Array<UInt8> = [0,0,0,0,0,0,0,0,0,0,0,0]
            let nIv = Data(iv)
            let gcmEnc = GCM(iv: nIv.bytes, mode: .detached)
            var enc = try? AES(key: data!.bytes, blockMode: gcmEnc, padding: .noPadding).makeEncryptor()
            let arrStr = ["My name is tarun"] // Working
            //let arrStr = ["tarun"] // Not working for this string
            var ciphertext = Array<UInt8>()
            for txt in arrStr{
                 let ciperText = try? enc?.update(withBytes: Array(txt.utf8)) // Getting nil for small string.
                 ciphertext.append(contentsOf: ciperText!)
            }
            var res = try? enc?.finish()
            gcmEnc.authenticationTag = self.randomGenerateBytes(count: 16)?.bytes
            res?.append(contentsOf: (gcmEnc.authenticationTag)!)
            let cipherData = Data(ciphertext) + Data(res!)
            let strEnc = String(decoding: cipherData, as: UTF8.self)
            print(strEnc)
        
            // Decryption start from here
            do {
                let gcmDec = GCM.init(iv: nIv.bytes, additionalAuthenticatedData: nil, tagLength: 16, mode: .detached)
                var aesDec = try! AES(key: data!.bytes, blockMode: gcmDec, padding: .noPadding).makeDecryptor()
                let tag_length = 16
                let encData = cipherData.subdata(in: 0..<cipherData.count - tag_length)
                let tag = cipherData.subdata(in: encData.count ..< cipherData.count)
                
                let decData = try? aesDec.update(withBytes: encData.bytes) //Getting nil here for small string
                let strData = String(decoding: decData!, as: UTF8.self)
                print(strData)
                
                do{
                    var res = try? aesDec.finish(withBytes: tag.bytes)
                    res?.append(contentsOf: tag)
                    
                }catch{
                    
                }
                
            } catch {
                // failed
            }
    }
    }
    func randomGenerateBytes(count: Int) -> Data? {
        let bytes = UnsafeMutableRawPointer.allocate(byteCount: count, alignment: 1)
        defer { bytes.deallocate() }
        let status = CCRandomGenerateBytes(bytes, count)
        guard status == kCCSuccess else { return nil }
        return Data(bytes: bytes, count: count)
    }
2

There are 2 best solutions below

1
On

I don't believe GCM requires PADDING. Here is an example pretty much straight from the NODE.JS documentation that works fine and does not use padding. The line below will show the Ciphertext length is 5

I have done the same with Ruby, Java, Go, and others and none require padding or the input value to be a multiple of 16 bytes like the Swift library seems to require. Anyone else help confirm this is a bug in Swift implementation of GCM?

const crypto = require('crypto');

const key = '12345678901234567890123456789012';
const iv = '000000000000'

const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);  
const plaintext = 'Hello';  
const ciphertext = cipher.update(plaintext, 'utf8');  
**console.log("ciphertext length %d", ciphertext.length)**  

cipher.final();  
const tag = cipher.getAuthTag();

const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);  
decipher.setAuthTag(tag);  
const receivedPlaintext = decipher.update(ciphertext, null, 'utf8');  

try {
  decipher.final();
} catch (err) {
  console.error('Authentication failed!');
  return;
}

console.log(receivedPlaintext);
2
On

There's nothing wrong with aes-256-gcm implementation in CryptoSwift, as some of the commenters have suggested, you just have some bugs in your code. Hopefully the following will help you out.

I'm just going to call it GCM below for brevity.

GCM encryption takes as input the plaintext, a key, and an initialization vector and produces ciphertext and an authentication tag. In your code, you set the authentication tag to random bytes, overwriting the authentication tag.

I think it's a bit clearer if you break your code up into some functions, each with a clearly defined purpose. I also stripped away some of the conversions from Data to and from [UInt8] for clarity.

Here's what the encryption function would look like:

func enc(plainText: [String], key: [UInt8], iv: [UInt8]) throws -> (cipherText: [UInt8], authenticationTag: [UInt8]?)
{
    let gcmEnc = GCM(iv: iv, mode: .detached)
    var enc = try AES(key: key, blockMode: gcmEnc, padding: .noPadding).makeEncryptor()
    var ciphertext = Array<UInt8>()
    for txt in plainText {
         ciphertext  += try enc.update(withBytes: Array(txt.utf8))
    }
    ciphertext += try enc.finish()
    return (ciphertext, gcmEnc.authenticationTag)
}

When you're decrypting GCM you need to pass in the ciphertext, key, initialization vector and the authentication tag. That would look like this:

func dec(cipherText: [UInt8], authenticationTag: [UInt8]?, key: [UInt8], iv: [UInt8]) throws -> [UInt8]? {
    let tagLength = authenticationTag?.count ?? 0
    let gcmDec = GCM.init(iv: iv, additionalAuthenticatedData: nil, tagLength: tagLength, mode: .detached)
    gcmDec.authenticationTag = authenticationTag
    var aesDec = try AES(key: key, blockMode: gcmDec, padding: .noPadding).makeDecryptor()
    var decData = try aesDec.update(withBytes: cipherText)
    decData += try aesDec.finish()
    return decData
}

In both cases, you need to make sure that you append the output of the finish call to the ciphertext or plaintext. This is particularly important with small amounts of data as the update method may produce nothing!

With these two functions written you can rewrite your test function as follows:

func encAndDec(){
    do {
        guard let key =  Data.init(base64Encoded: "12345678901234567890123456789012".base64Encoded()!)
            else {
                fatalError("Failed to create key")
        }
        let iv : Array<UInt8> = [0,0,0,0,
                                 0,0,0,0,
                                 0,0,0,0]

        //let arrStr = ["My name is tarun"] // Working
        let arrStr = ["tarun"] // Not working for this string
        
        let (cipherText, authenticationTag) = try enc(plainText: arrStr, key: key.bytes, iv: iv)
        guard let decrypedPlainText = try dec(cipherText: cipherText,
                                              authenticationTag: authenticationTag, key: key.bytes, iv: iv) else {
            fatalError("Decryption return nil")
        }
        guard let decryptedString = String(bytes: decrypedPlainText, encoding: .utf8) else {
            fatalError("Failed to convert plaintext to string using UTF8 encoding.")
        }

        print("Decrypted Plaintext: \(decryptedString)")

    }
    catch let e {
        print("EXCEPTION: \(e)")
    }
}

If you run this you'll find it produces the expected output.

The complete example code can be found at: https://gist.github.com/iosdevzone/45456d2911bf2422bc4a6898876ba0ab