Search code examples
iosswiftaesencryption-symmetric

CCCrypt working with pieces of files AES CBC: garbage to end of files on encryption-decryption cycle


Although I was able to get data encryption and decryption (AES128CBC) working easily with CCCrypt based on some of the great examples of Zaph and others here, I have had two strange problems with CCCrypt when working with encrypting/decrypting files.

1) Upon encrypting and then decrypting a file, I am getting extra garbage at the end of my files, and it is different depending on the file. A hex dump of one original file and the result after an encryption and decryption had and extra "0B 0B 0B 0B 0B 0B 0B 0B 0B 0B 0B" another file had an extra "05 05 05 05 05" at the end. A diff at the command line for both resultant files reports "No newline at end of file". Other than these problems (and problems working with the files relating to these problems like NSJSONSerialization refusing to parse) everything appears to be working. What is causing this frustrating problem?

2) In order to eventually accommodate very large files I have broken up files into chunks and then used the last 16bytes of the previous chunk to as the IV for the next (since this is the way CBC works anyway, right?). Strangely, on chunks divisible by the key size, using kCCOption PKCS7Padding is causing problems. So that the IV ends up not being the same when decrypting as encrypting between these chunks. When I just set the options to zero for all the chunks including the last which is likely not to be divisible by the key size, I get an exception. Can anyone help me understand what this issue is? I just use a condition to avoid the issue, but I don't understand the exception, and perhaps it relates to question 1.

    let fileSize = getFileSizeFromPath(filePath)
    println("filesize = \(fileSize)")
    while file != nil {
        if let inputBuffer = file?.readDataOfLength(oneMegaByte) {
            if inputBuffer.length == 0 {
                file?.closeFile()
                break
            } else {
                println("input buffer length: \(inputBuffer.length)")
                if let outputBuffer = inputBuffer.AES128CBC(key: key, iv: iv, encryptionOp: encrypt) {
                    println("output buffer length: \(outputBuffer.length)")
                    outFile?.writeData(outputBuffer)
                    println("input file offset:\(file!.offsetInFile) output file offset:\(outFile!.offsetInFile)")
                    if encrypt {
                        let range = NSMakeRange((outputBuffer.length - const.keyLength), const.keyLength)
                        iv = outputBuffer.subdataWithRange(range)
                        println("range of iv for next chunk:\(range), iv value: \(iv)")
                    } else {
                        let range = NSMakeRange((inputBuffer.length - const.keyLength), const.keyLength)
                        iv = inputBuffer.subdataWithRange(range)
                        println("range of iv for next chunk:\(range), iv value: \(iv)")
                    }
                } else {
                    file?.closeFile()
                    println("problem encrypting data")
                    break
                }
            }
        } else {
            file?.closeFile()
            break
        }

And the method I've added to an NSData extension to encrypt and decrypt:

func AES128CBC(#key: NSData, iv: NSData, encryptionOp: Bool) -> NSData? {
    if key.length != 16 || iv.length != 16 || key.bytes == iv.bytes {
        return nil
    }

    let data = self
    let dataLength = UInt(data.length)
    let cPtrToData = UnsafePointer<UInt8>(data.bytes)
    let cPtrToIVData = UnsafePointer<UInt8>(iv.bytes)
    let cPtrTokeyData = UnsafePointer<UInt8>(key.bytes)
    let keySize = size_t(kCCKeySizeAES128)

    let buffer: NSMutableData! = NSMutableData(length: Int(dataLength) + kCCBlockSizeAES128)
    if buffer == nil { return nil }
    var cPtrTobuffer = UnsafeMutablePointer<UInt8>(buffer.mutableBytes)
    let bufferSize = size_t(buffer.length)

    var operation: CCOperation
    if encryptionOp {
        operation = UInt32(kCCEncrypt)
    } else {
        operation = UInt32(kCCDecrypt)
    }

    let algoritm: CCAlgorithm = UInt32(kCCAlgorithmAES128)
    var options: CCOptions
    if dataLength % UInt(const.keyLength) != 0 {
        options = UInt32(kCCOptionPKCS7Padding)
    } else {
        options = 0
    }
        var encryptedByteCount: UInt = 0

    var operationResult = CCCrypt(operation, algoritm, options, cPtrTokeyData, keySize, cPtrToIVData, cPtrToData, dataLength, cPtrTobuffer, bufferSize, &encryptedByteCount)

    if UInt32(operationResult) == UInt32(kCCSuccess) {
        println("encrypted / decrypted byte count: \(encryptedByteCount)")
        buffer.length = Int(encryptedByteCount)
        return buffer
    }
    println("Error: \(operationResult)")
    return nil
}

Solution

  • The extra bytes are the PKCS#7 padding. Encryption is block based so bytes must be added on encryption and later removed on decryption to achieve this. Per PKCS#7 the extra bytes are the number of bytes added. These additional bytes: "05 05 05 05 05" indicate 5 bytes of padding were added.

    If PKCS#7 padding is specified and the input data is an exact multiple of the block size another block is added (and will be all padding bytes). This must be sent of there will be padding errors on decryption. If you know on both encryption and decryption that the data is a multiple of the block size you can skip PKCS#7 padding. In the OP's case this could be skipped on all encryption segments except the last if the intermediate segments were all a multiple of block size. AES uses 128-bit (16-byte) blocks.