Why I cant decode AES-CTR in python

Question:

Im reversing a site to write an api for it, and there is an AES encryption. I figured out that this is AES CTR mode and that 8 byte nonce is prefix for ciphertext. However, when I try decoding with PyCrypto I get nonsence results.

This is site’s js:

void 0 === String.prototype.utf8Encode && (String.prototype.utf8Encode = function() {
    return unescape(encodeURIComponent(this))
}
),
void 0 === String.prototype.utf8Decode && (String.prototype.utf8Decode = function() {
    try {
        return decodeURIComponent(escape(this))
    } catch (e) {
        return this
    }
}
),
void 0 === String.prototype.base64Encode && (String.prototype.base64Encode = function() {
    if ("undefined" != typeof btoa)
        return btoa(this);
    if ("undefined" != typeof Buffer)
        return new Buffer(this,"binary").toString("base64");
    throw new Error("No Base64 Encode")
}
),
void 0 === String.prototype.base64Decode && (String.prototype.base64Decode = function() {
    if ("undefined" != typeof atob)
        return atob(this);
    if ("undefined" != typeof Buffer)
        return new Buffer(this,"base64").toString("binary");
    throw new Error("No Base64 Decode")
}
)
var aes = {
    cipher: function(e, t) {
        for (var i = t.length / 4 - 1, a = [[], [], [], []], n = 0; n < 16; n++)
            a[n % 4][Math.floor(n / 4)] = e[n];
        a = aes.addRoundKey(a, t, 0, 4);
        for (var o = 1; o < i; o++)
            a = aes.subBytes(a, 4),
            a = aes.shiftRows(a, 4),
            a = aes.mixColumns(a, 4),
            a = aes.addRoundKey(a, t, o, 4);
        a = aes.subBytes(a, 4),
        a = aes.shiftRows(a, 4),
        a = aes.addRoundKey(a, t, i, 4);
        var r = new Array(16);
        for (n = 0; n < 16; n++)
            r[n] = a[n % 4][Math.floor(n / 4)];
        return r
    },
    keyExpansion: function(e) {
        for (var t = e.length / 4, i = t + 6, a = new Array(4 * (i + 1)), n = new Array(4), o = 0; o < t; o++) {
            var r = [e[4 * o], e[4 * o + 1], e[4 * o + 2], e[4 * o + 3]];
            a[o] = r
        }
        for (o = t; o < 4 * (i + 1); o++) {
            a[o] = new Array(4);
            for (var s = 0; s < 4; s++)
                n[s] = a[o - 1][s];
            if (o % t == 0) {
                n = aes.subWord(aes.rotWord(n));
                for (s = 0; s < 4; s++)
                    n[s] ^= aes.rCon[o / t][s]
            } else
                t > 6 && o % t == 4 && (n = aes.subWord(n));
            for (s = 0; s < 4; s++)
                a[o][s] = a[o - t][s] ^ n[s]
        }
        return a
    },
    subBytes: function(e, t) {
        for (var i = 0; i < 4; i++)
            for (var a = 0; a < t; a++)
                e[i][a] = aes.sBox[e[i][a]];
        return e
    },
    shiftRows: function(e, t) {
        for (var i = new Array(4), a = 1; a < 4; a++) {
            for (var n = 0; n < 4; n++)
                i[n] = e[a][(n + a) % t];
            for (n = 0; n < 4; n++)
                e[a][n] = i[n]
        }
        return e
    },
    mixColumns: function(e, t) {
        for (var i = 0; i < 4; i++) {
            for (var a = new Array(4), n = new Array(4), o = 0; o < 4; o++)
                a[o] = e[o][i],
                n[o] = 128 & e[o][i] ? e[o][i] << 1 ^ 283 : e[o][i] << 1;
            e[0][i] = n[0] ^ a[1] ^ n[1] ^ a[2] ^ a[3],
            e[1][i] = a[0] ^ n[1] ^ a[2] ^ n[2] ^ a[3],
            e[2][i] = a[0] ^ a[1] ^ n[2] ^ a[3] ^ n[3],
            e[3][i] = a[0] ^ n[0] ^ a[1] ^ a[2] ^ n[3]
        }
        return e
    },
    addRoundKey: function(e, t, i, a) {
        for (var n = 0; n < 4; n++)
            for (var o = 0; o < a; o++)
                e[n][o] ^= t[4 * i + o][n];
        return e
    },
    subWord: function(e) {
        for (var t = 0; t < 4; t++)
            e[t] = aes.sBox[e[t]];
        return e
    },
    rotWord: function(e) {
        for (var t = e[0], i = 0; i < 3; i++)
            e[i] = e[i + 1];
        return e[3] = t,
        e
    },
    sBox: [99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118, 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192, 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21, 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117, 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132, 83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207, 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, 140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22],
    rCon: [[0, 0, 0, 0], [1, 0, 0, 0], [2, 0, 0, 0], [4, 0, 0, 0], [8, 0, 0, 0], [16, 0, 0, 0], [32, 0, 0, 0], [64, 0, 0, 0], [128, 0, 0, 0], [27, 0, 0, 0], [54, 0, 0, 0]],
    encrypt: function(e, t, i) {
        if (128 != i && 192 != i && 256 != i)
            throw new Error("Key size is not 128 / 192 / 256");
        e = String(e).utf8Encode(),
        t = String(t).utf8Encode();
        for (var a = i / 8, n = new Array(a), o = 0; o < a; o++)
            n[o] = o < t.length ? t.charCodeAt(o) : 0;
        var r = aes.cipher(n, aes.keyExpansion(n));
        r = r.concat(r.slice(0, a - 16));
        var s = new Array(16)
          , l = (new Date).getTime()
          , d = l % 1e3
          , c = Math.floor(l / 1e3)
          , u = Math.floor(65535 * Math.random());
        for (o = 0; o < 2; o++)
            s[o] = d >>> 8 * o & 255;
        for (o = 0; o < 2; o++)
            s[o + 2] = u >>> 8 * o & 255;
        for (o = 0; o < 4; o++)
            s[o + 4] = c >>> 8 * o & 255;
        var p = "";
        for (o = 0; o < 8; o++)
            p += String.fromCharCode(s[o]);
        for (var h = aes.keyExpansion(r), f = Math.ceil(e.length / 16), g = "", m = 0; m < f; m++) {
            for (var v = 0; v < 4; v++)
                s[15 - v] = m >>> 8 * v & 255;
            for (v = 0; v < 4; v++)
                s[15 - v - 4] = m / 4294967296 >>> 8 * v;
            var y = aes.cipher(s, h)
              , b = m < f - 1 ? 16 : (e.length - 1) % 16 + 1
              , S = new Array(b);
            for (o = 0; o < b; o++)
                S[o] = y[o] ^ e.charCodeAt(16 * m + o),
                S[o] = String.fromCharCode(S[o]);
            g += S.join(""),
            "undefined" != typeof WorkerGlobalScope && self instanceof WorkerGlobalScope && m % 1e3 == 0 && self.postMessage({
                progress: m / f
            })
        }
        return g = (p + g).base64Encode()
    },
    decrypt: function(e, t, i) {
        if (128 != i && 192 != i && 256 != i)
            throw new Error("Key size is not 128 / 192 / 256");
        e = String(e).base64Decode(),
        t = String(t).utf8Encode();
        for (var a = i / 8, n = new Array(a), o = 0; o < a; o++)
            n[o] = o < t.length ? t.charCodeAt(o) : 0;
        var r = aes.cipher(n, aes.keyExpansion(n));
        r = r.concat(r.slice(0, a - 16));
        var s = new Array(8)
          , l = e.slice(0, 8);
        for (o = 0; o < 8; o++)
            s[o] = l.charCodeAt(o);
        for (var d = aes.keyExpansion(r), c = Math.ceil((e.length - 8) / 16), u = new Array(c), p = 0; p < c; p++)
            u[p] = e.slice(8 + 16 * p, 8 + 16 * p + 16);
        e = u;
        var h = "";
        for (p = 0; p < c; p++) {
            for (var f = 0; f < 4; f++)
                s[15 - f] = p >>> 8 * f & 255;
            for (f = 0; f < 4; f++)
                s[15 - f - 4] = (p + 1) / 4294967296 - 1 >>> 8 * f & 255;
            var g = aes.cipher(s, d)
              , m = new Array(e[p].length);
            for (o = 0; o < e[p].length; o++)
                m[o] = g[o] ^ e[p].charCodeAt(o),
                m[o] = String.fromCharCode(m[o]);
            h += m.join(""),
            "undefined" != typeof WorkerGlobalScope && self instanceof WorkerGlobalScope && p % 1e3 == 0 && self.postMessage({
                progress: p / c
            })
        }
        return h = h.utf8Decode()
    }
};


var key = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
console.log(aes.encrypt("secret", key, 256))

For example whith message "secret" and key ‘a’*32 we get this cipher text 0AO5xapH5mJmMROu37A=

Now, when trying to decode it in python I get nothing

from Crypto.Cipher import AES
from base64 import b64decode

ciphertext = b64decode("0AO5xapH5mJmMROu37A=")
nc = ciphertext[:8]
data = ciphertext[8:]

key = b'a'*32

aes = AES.new(key=key, mode=AES.MODE_CTR, nonce = nc, initial_value=0) 

res = aes.decrypt(data)
print(res) # b'xcdx96Yxdcx0fx05'

I get some random bytes when "secret" expected.

And I dont really understand what am I doing wrong. So please help me if possible. Thanks in advance!

Asked By: ParaEagle

||

Answers:

Decryption fails because the JavaScript code actually applies a key derivation that is not considered in the Python code.
The key derivation is done at the very beginning of the encrypt() and decrypt() methods, r is the derived key.
The logic applied is:

  • The password is first brought to the length of the desired key size by truncating it if it is too long or padding it with 0x00 values if it is too short.

  • Encrypting the first 16 bytes of the length-corrected password with the AES primitive, using the length-corrected password as key, gives a 16 bytes key.
    Note that encrypting 16 bytes with the AES primitive is functionally equivalent to encrypting with AES in ECB mode without padding.

  • This key is expanded to the desired key size by adding the appropriate number of bytes of the beginning of the key to the end of the key.

This can be implemented in Python as follows:

def pad(data, ks):
    pad_len = (ks - (len(data) % ks)) % ks 
    return data + (b'x00' * pad_len)

def kdf(pwd, keySize): 
    if keySize != 16 and keySize != 24 and keySize != 32:
        raise ValueError("Wrong keysize") 
    keyPadded = pwd[:keySize] if len(pwd) >= keySize else pad(pwd, keySize)
    aes = AES.new(key=keyPadded, mode=AES.MODE_ECB) 
    key = aes.encrypt(keyPadded[:16])
    if keySize > 16:
        key = (key + key)[:keySize]
    return key

Test (with the data provided by the OP):

from Crypto.Cipher import AES
from base64 import b64decode

ciphertext = b64decode("0AO5xapH5mJmMROu37A=")
nc = ciphertext[:8]
data = ciphertext[8:]

keySize = 32
pwd = b'a'*32
key = kdf(pwd, keySize) 
aes = AES.new(key=key, mode=AES.MODE_CTR, nonce=nc, initial_value=0) 
res = aes.decrypt(data)

print(res) # b'secret'
Answered By: Topaco