// The PasspetAES XPCOM component implements AES (see FIPS 197).

// Shorthand for getting XPConnect interfaces, classes, objects, and services.
const XPI = Components.interfaces, XPC = Components.classes;
function XPC_(c) { return XPC[c.match(/^@/) ? c : '@mozilla.org/' + c]; }
function XPO(c, i) { return XPC_(c) ? XPC_(c).createInstance(i) : null; }
function XPS(c, i) { return XPC_(c) ? XPC_(c).getService(i) : null; }
function XQI(o, i) { return o.QueryInterface(i); }

const XPR = Components.results;
const CID = Components.ID('{eef40e3a-43c2-5633-bca4-72878537f738}');
const NAME = 'Passpet AES Object';
const CONTRACT = '@passpet.org/aes;1';

// The module registers and unregisters the PasspetAES component interface.
function NSGetModule(manager, filespec) {
    return new function() {
        this.registerSelf = function(manager, filespec, location, type) {
            manager = XQI(manager, XPI.nsIComponentRegistrar);
            manager.registerFactoryLocation(
                CID, NAME, CONTRACT, filespec, location, type);
        };

        this.unregisterSelf = function(manager, location, type) {
            manager = XQI(manager, XPI.nsIComponentRegistrar);
            manager.unregisterFactoryLocation(CID, location);
        };

        this.canUnload = function(manager) {
            return true;
        };

        this.getClassObject = function(manager, cid, iid) {
            if (!iid.equals(XPI.nsIFactory)) throw XPR.NS_ERROR_NOT_IMPLEMENTED;
            if (!cid.equals(CID)) throw XPR.NS_ERROR_NO_INTERFACE;
            return new function() {
                this.createInstance = function(outer, iid) {
                    if (outer != null) throw XPR.NS_ERROR_NO_AGGREGATION;
                    return XQI(new class(), iid);
                };
            };
        };
    };
};

const class = function() {
    var Nk, Nb, Nr, w;
    var keybits = 0, key = '';

    const chr = String.fromCharCode;

    // S-box array (FIPS 197 page 6).
    S = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5,
         0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
         0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0,
         0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
         0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc,
         0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
         0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a,
         0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
         0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0,
         0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
         0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b,
         0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
         0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85,
         0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
         0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5,
         0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
         0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17,
         0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
         0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88,
         0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
         0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c,
         0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
         0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9,
         0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
         0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6,
         0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
         0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e,
         0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
         0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94,
         0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
         0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68,
         0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16];
    InvS = [];
    for (var i = 0; i < 256; i++) InvS[S[i]] = i;

    function byte(n) {
        return n & 0xff;
    }

    function word(n) {
        return n & 0xffffffff;
    }
    
    function toword(a, b, c, d) {
        return (byte(a)<<24) | (byte(b)<<16) | (byte(c)<<8) | byte(d);
    }

    function SubWord(w) {
        var a = byte(w>>24), b = byte(w>>16), c = byte(w>>8), d = byte(w);
        return toword(S[a], S[b], S[c], S[d]);
    }

    function RotWord(w) {
        var a = byte(w>>24), b = byte(w>>16), c = byte(w>>8), d = byte(w);
        return toword(b, c, d, a);
    }

    // Reduce a polynomial modulo x^8 + x^4 + x^3 + x + 1.
    function reduce(n) {
        var bit = 1<<8;
        var modulus = (1<<8) + (1<<4) + (1<<3) + (1<<1) + (1<<0);
        while (bit <= n) {
            bit <<= 1;
            modulus <<= 1;
        }
        while (bit >= (1<<8)) {
            if (n & bit) n ^= modulus;
            bit >>= 1;
            modulus >>= 1;
        }
        return n;
    }

    // Multiply two polynomials modulo x^8 + x^4 + x^3 + x + 1.
    function mult(a, b) {
        var product = 0, bit = 1;
        while (bit <= a) {
            if (a & bit) product ^= b*bit;
            bit <<= 1;
        }
        return reduce(product);
    }

    // Rounding constants.
    var x = 1;
    var Rcon = [0, x<<24];
    for (var i = 2; i < 256; i++) {
        x = reduce(x<<1);
        Rcon.push(x<<24);
    }

    // Key expansion according to FIPS 197, page 19.
    function KeyExpansion(K, Nk, Nb, Nr) {
        var w = [];

        // Convert K (a string) into the first Nk words of the schedule.
        for (var i = 0; i < Nk; i++) {
            var a = K.charCodeAt(i*4);
            var b = K.charCodeAt(i*4 + 1);
            var c = K.charCodeAt(i*4 + 2);
            var d = K.charCodeAt(i*4 + 3);
            w.push(toword(a, b, c, d));
        }

        for (var i = Nk; i < Nb*(Nr + 1); i++) {
            var temp = w[i - 1];
            var ih = Math.floor(i/Nk), il = i % Nk;
            if (il == 0) temp = SubWord(RotWord(temp)) ^ Rcon[ih];
            else if (Nk > 6 && il == 4) temp = SubWord(temp);
            w[i] = w[i - Nk] ^ temp;
        }

        return w;
    }

    // Transformations used in the cipher.
    function SubBytes(state) {
        return [S[state[0]], S[state[1]], S[state[2]], S[state[3]],
                S[state[4]], S[state[5]], S[state[6]], S[state[7]],
                S[state[8]], S[state[9]], S[state[10]], S[state[11]],
                S[state[12]], S[state[13]], S[state[14]], S[state[15]]];
    }

    function InvSubBytes(state) {
        return [
            InvS[state[0]], InvS[state[1]], InvS[state[2]], InvS[state[3]],
            InvS[state[4]], InvS[state[5]], InvS[state[6]], InvS[state[7]],
            InvS[state[8]], InvS[state[9]], InvS[state[10]], InvS[state[11]],
            InvS[state[12]], InvS[state[13]], InvS[state[14]], InvS[state[15]]
        ];
    }

    function ShiftRows(state) {
        return [state[0], state[5], state[10], state[15],
                state[4], state[9], state[14], state[3],
                state[8], state[13], state[2], state[7],
                state[12], state[1], state[6], state[11]];
    }

    function InvShiftRows(state) {
        return [state[0], state[13], state[10], state[7],
                state[4], state[1], state[14], state[11],
                state[8], state[5], state[2], state[15],
                state[12], state[9], state[6], state[3]];
    }

    function MixColumn(c0, c1, c2, c3) {
        var m0 = mult(c0, 2) ^ mult(c1, 3) ^ c2 ^ c3;
        var m1 = c0 ^ mult(c1, 2) ^ mult(c2, 3) ^ c3;
        var m2 = c0 ^ c1 ^ mult(c2, 2) ^ mult(c3, 3);
        var m3 = mult(c0, 3) ^ c1 ^ c2 ^ mult(c3, 2);
        return [m0, m1, m2, m3];
    }

    function MixColumns(state) {
        return MixColumn(state[0], state[1], state[2], state[3]).concat(
               MixColumn(state[4], state[5], state[6], state[7])).concat(
               MixColumn(state[8], state[9], state[10], state[11])).concat(
               MixColumn(state[12], state[13], state[14], state[15]));
    }

    function InvMixColumn(c0, c1, c2, c3) {
        var m0 = mult(c0, 14) ^ mult(c1, 11) ^ mult(c2, 13) ^ mult(c3, 9);
        var m1 = mult(c0, 9) ^ mult(c1, 14) ^ mult(c2, 11) ^ mult(c3, 13);
        var m2 = mult(c0, 13) ^ mult(c1, 9) ^ mult(c2, 14) ^ mult(c3, 11);
        var m3 = mult(c0, 11) ^ mult(c1, 13) ^ mult(c2, 9) ^ mult(c3, 14);
        return [m0, m1, m2, m3];
    }

    function InvMixColumns(state) {
        return InvMixColumn(state[0], state[1], state[2], state[3]).concat(
               InvMixColumn(state[4], state[5], state[6], state[7])).concat(
               InvMixColumn(state[8], state[9], state[10], state[11])).concat(
               InvMixColumn(state[12], state[13], state[14], state[15]));
    }

    function AddRoundKey(state, wi) {
        var w0 = w[wi], w1 = w[wi + 1], w2 = w[wi + 2], w3 = w[wi + 3];
        var a = byte(w0>>24), b = byte(w0>>16), c = byte(w0>>8), d = byte(w0);
        var e = byte(w1>>24), f = byte(w1>>16), g = byte(w1>>8), h = byte(w1);
        var i = byte(w2>>24), j = byte(w2>>16), k = byte(w2>>8), l = byte(w2);
        var m = byte(w3>>24), n = byte(w3>>16), o = byte(w3>>8), p = byte(w3);
        return [state[0] ^ a, state[1] ^ b, state[2] ^ c, state[3] ^ d,
                state[4] ^ e, state[5] ^ f, state[6] ^ g, state[7] ^ h,
                state[8] ^ i, state[9] ^ j, state[10] ^ k, state[11] ^ l,
                state[12] ^ m, state[13] ^ n, state[14] ^ o, state[15] ^ p];
    }

    this.keybits getter = function() { return keybits; }
    this.blockbytes getter = function() { return 4*Nb; }

    // Cipher setup.
    this.init = function(keybits0, key0) {
        if (keybits != 0) throw XPR.NS_ERROR_ALREADY_INITIALIZED;

        keybits = keybits0;
        key = key0;
        if (keybits == 128) {
            Nk = 4;
            Nb = 4;
            Nr = 10;
        } else if (keybits == 192) {
            Nk = 6;
            Nb = 4;
            Nr = 12;
        } else if (keybits == 256) {
            Nk = 8;
            Nb = 4;
            Nr = 14;
        } else {
            throw XPR.NS_ERROR_ILLEGAL_VALUE;
        }
        w = KeyExpansion(key, Nk, Nb, Nr);
    }

    // Ciphering routines.
    function encipherBlock(block) {
        if (keybits == 0) throw XPR.NS_ERROR_NOT_INITIALIZED;
        state = AddRoundKey(block, 0);
        for (var round = 1; round < Nr; round++) {
            state = SubBytes(state);
            state = ShiftRows(state);
            state = MixColumns(state);
            state = AddRoundKey(state, round*Nb);
        }
        state = SubBytes(state);
        state = ShiftRows(state);
        state = AddRoundKey(state, Nr*Nb);
        return state;
    }

    function decipherBlock(block) {
        if (keybits == 0) throw XPR.NS_ERROR_NOT_INITIALIZED;
        state = AddRoundKey(block, Nr*Nb);
        for (var round = Nr - 1; round > 0; round--) {
            state = InvShiftRows(state);
            state = InvSubBytes(state);
            state = AddRoundKey(state, round*Nb);
            state = InvMixColumns(state);
        }
        state = InvShiftRows(state);
        state = InvSubBytes(state);
        state = AddRoundKey(state, 0);
        return state;
    }

    function tostring(block) {
        var string = '';
        for (var i = 0; i < block.length; i++) {
            string += chr(block[i]);
        }
        return string;
    }

    function toblock(string) {
        var block = [];
        for (var i = 0; i < string.length; i++) {
            block.push(string.charCodeAt(i));
        }
        return block;
    }

    this.encipher = function(string) {
        return tostring(encipherBlock(toblock(string)));
    }

    this.decipher = function(string) {
        return tostring(decipherBlock(toblock(string)));
    }

    // Message authentication code.
    function blockshift(block) {
        var shifted = [], carry = 0;
        for (var i = block.length - 1; i >= 0; i--) {
            shifted[i] = byte(block[i]<<1) + carry;
            carry = (block[i] & 0x80) ? 1 : 0;
        }
        return shifted;
    }

    function blockxor(a, b) {
        var result = [];
        for (var i = 0; i < a.length; i++) {
            result.push(a[i] ^ b[i]);
        }
        return result;
    }

    function pad(string, blockbytes) {
        padded = string + '\x80';
        while (padded.length % blockbytes) padded += '\x00';
        return padded;
    }

    function unpad(string) {
        var i = padded.length;
        while (padded.charCodeAt(i - 1) == 0) i--;
        padded = padded.substring(0, i);
        if (padded.charCodeAt(padded.length - 1) != '\x80') {
            throw XPR.NS_ERROR_ILLEGAL_VALUE;
        }
        return padded.substring(0, padded.length - 1);
    }

    function toblocks(string, blockbytes) {
        if (string.length % blockbytes) {
            throw XPR.NS_ERROR_ILLEGAL_VALUE;
        }
        var blocks = [];
        for (var i = 0; i < string.length; i += blockbytes) {
            var block = [];
            for (var j = 0; j < blockbytes; j++) {
                block.push(string.charCodeAt(i + j));
            }
            blocks.push(block);
        }
        return blocks;
    }

    this.getMAC = function(data) {
        if (keybits != 128) throw XPR.NS_ERROR_ILLEGAL_VALUE;

        // Generate the two subkeys.
        var zero = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
        var L = encipherBlock(zero);
        K1 = blockshift(L);
        if (L[0] & 0x80) K1[15] ^= 0x87;
        K2 = blockshift(K1);
        if (K1[0] & 0x80) K2[15] ^= 0x87;

        // Decide which key to use.
        var n = Math.ceil(data.length/16), flag;
        if (n == 0) {
            n == 1;
            flag = false;
        } else {
            flag = (data.length % 16 == 0);
        }
        var blocks, M_last;
        if (flag) {
            blocks = toblocks(data, 16);
            M_last = blockxor(blocks[blocks.length - 1], K1);
        } else {
            blocks = toblocks(pad(data, 16), 16);
            M_last = blockxor(blocks[blocks.length - 1], K2);
        }

        // Compute the MAC.
        var X = zero, Y, T;
        for (var i = 0; i < n - 1; i++) {
            Y = blockxor(X, blocks[i]);
            X = encipherBlock(Y);
        }
        Y = blockxor(X, M_last);
        T = encipherBlock(Y);

        // Convert to a string.
        return tostring(T);
    }

    // This method implements the nsISupports interface.
    this.QueryInterface = function(iid) {
        if (iid.equals(XPI.IPasspetAES)) return this;
        if (iid.equals(XPI.nsISupports)) return this;
        throw XPR.NS_ERROR_NO_INTERFACE;
    };
};

