CryptoSwift with AES128 CTR Mode - Buggy counter increment?

Michael S Source

i've encountered a problem on the CryptoSwift-API (krzyzanowskim) while using AES128 with the CTR-Mode and my test function (nullArrayBugTest()) that produces on specific counter values (between 0 and 25 = on 13 and 24) a wrong array count that should usually be 16! Even if I use the manually incremented "iv_13" with the buggy value 13 instead of the default "iv_0" and the counter 13... Test it out to get an idea what I mean.

  func nullArrayBugTest() {
    var ctr:CTR
    let nilArrayToEncrypt = Data(hex: "00000000000000000000000000000000")
    let key_ = Data(hex: "000a0b0c0d0e0f010203040506070809")
    let iv_0:  Array<UInt8> = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]
    //let iv_13:  Array<UInt8> = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x1c]
    var decryptedNilArray = [UInt8]()

    for i in 0...25 {
        ctr = CTR(iv: iv_0, counter: i)
        do {
            let aes = try AES(key: key_.bytes, blockMode: ctr)
            decryptedNilArray = try aes.decrypt([UInt8](nilArrayToEncrypt))
            print("AES_testcase_\(i) for ctr: \(ctr) withArrayCount: \(decryptedNilArray.count)")
        }catch {
            print("De-/En-CryptData failed with: \(error)")

Output with buggy values

The question why I always need the encrypted array with 16 values is not important :D.

Does anybody know why the aes.decrypt()-function handles that like I received?

Thanks for your time.

Michael S.



answered 6 days ago Rob Napier #1

CryptoSwift defaults to PKCS#7 padding. Your resulting plaintexts have invalid padding. CryptoSwift ignores padding errors, which IMO is a bug, but that's how it's implemented. (All the counters that you're considering "correct" should really have failed to decrypt at all.) (I spoke this over with Marcin and he reminded me that even at this low level, it's normal to ignore padding errors to avoid padding oracle attacks. I'd forgotten that I do it this way too....)

That said, sometimes the padding will be "close enough" that CryptoSwift will try to remove padding bytes. It usually won't be valid padding, but it'll be close enough for CrypoSwift's test.

As an example, your first counter creates the following padded plaintext:

[233, 222, 112, 79, 186, 18, 139, 53, 208, 61, 91, 0, 120, 247, 187, 254]

254 > 16, so CryptoSwift doesn't try to remove padding.

For a counter of 13, the following padded plaintext is returned:

[160, 140, 187, 255, 90, 209, 124, 158, 19, 169, 164, 110, 157, 245, 108, 12]

12 < 16, so CryptoSwift removes 12 bytes, leaving 4. (This is not how PKCS#7 padding works, but it's how CryptoSwift works.)

The underlying problem is you're not decrypting something you encrypted. You're just running a static block through the decryption scheme.

If you don't want padding, you can request that:

let aes = try AES(key: key_.bytes, blockMode: ctr, padding: .noPadding)

This will return you what you're expecting.

Just in case there's any confusion by other readers: this use of CTR is wildly insecure and no part of it should be copied. I'm assuming that the actual encryption code doesn't work anything like this.

answered 6 days ago Marcin #2

I guess the encryption happens without the padding applied, but then u use padding to decrypt. To fix that, use the same technique on both sides. That said, this is a solution (@rob-napier answer is more detailed):

try AES(key: key_.bytes, blockMode: ctr, padding: .noPadding)

comments powered by Disqus