// The PasspetGCM XPCOM component implements GCM (see http://csrc.nist.gov/
// CryptoToolkit/modes/proposedmodes/gcm/gcm-revised-spec.pdf).

// 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('{48602656-5d61-518b-b136-40df6d99ae65}');
const NAME = 'Passpet GCM Object';
const CONTRACT = '@passpet.org/gcm;1';

// The module registers and unregisters the PasspetGCM 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 cipher = null, iv, H, Y0;

    const chr = String.fromCharCode;
    const zeroblock = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];

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

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

    function toblocks(string, blockbytes) {
        while (string.length % blockbytes) {
            string += '\0';
        }
        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;
    }

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

    function blockshift(block) {
        var shifted = [], carry = 0;
        for (var i = 0; i < block.length; i++) {
            shifted[i] = (carry*0x80) + (block[i]>>1);
            carry = block[i] & 1;
        }
        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 mult(X, Y) {
        // Multiply in GF(2^128) modulo x^128 + x^7 + x^2 + x + 1.
        var Z = zeroblock;
        var V = X;
        for (var i = 0; i < 16; i++) {
            for (var j = 0x80; j; j >>= 1) {
                if (Y[i] & j) Z = blockxor(Z, V);
                if (V[15] & 1) {
                    V = blockshift(V);
                    V[0] ^= 0xe1;
                } else {
                    V = blockshift(V);
                }
            }
        }
        return Z;
    }

    function encipher(block) {
        // Work around the inability of XPCOM to pass arrays.
        return toblock(cipher.encipher(tostring([block])));
    }

    this.initWithIV = function(cipher0, iv0) {
        if (cipher != null) throw XPR.NS_ERROR_ALREADY_INITIALIZED;
        cipher = cipher0;
        iv = toblock(iv0).slice(-12);
        H = encipher(zeroblock);
        Y0 = iv.concat([0, 0, 0, 1]);
    }

    this.initWithNonce = function(cipher0, nonce) {
        if (cipher != null) throw XPR.NS_ERROR_ALREADY_INITIALIZED;
        nonce = zeroblock.concat(toblock(nonce)).slice(-16);
        cipher = cipher0;
        iv = encipher(nonce).slice(-12);
        H = encipher(zeroblock);
        Y0 = iv.concat([0, 0, 0, 1]);
    }

    function incr(Y) {
        Y[15]++;
        if (Y[15] == 256) {
            Y[15] = 0;
            Y[14]++;
            if (Y[14] == 256) {
                Y[14] = 0;
                Y[13]++;
                if (Y[13] == 256) {
                    Y[13] = 0;
                    Y[12]++;
                    if (Y[12] == 256) Y[12] = 0;
                }
            }
        }
    }

    this.transform = function(data) {
        var Y = [], newblocks = [];
        for (var i = 0; i < 16; i++) Y.push(Y0[i]);
        var blocks = toblocks(data, 16);
        for (var i = 0; i < blocks.length; i++) {
            incr(Y);
            var C = blockxor(blocks[i], encipher(Y));
            newblocks.push(C);
        }
        return tostring(newblocks).substring(0, data.length);
    }

    function ghash(data) {
        var X = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
        var blocks = toblocks(data, 16);
        for (var i = 0; i < blocks.length; i++) {
            X = mult(blockxor(X, blocks[i]), H);
        }
        var l = data.length * 8;
        var lengthblock = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                           byte(l>>24), byte(l>>16), byte(l>>8), byte(l)];
        return mult(blockxor(X, lengthblock), H);
    }
    
    this.encrypt = function(data) {
        if (cipher == null) throw XPR.NS_ERROR_NOT_INITIALIZED;
        var C = this.transform(data);
        var T = blockxor(ghash(C), encipher(Y0));
        return C + tostring([T]);
    }

    this.decrypt = function(data) {
        if (cipher == null) throw XPR.NS_ERROR_NOT_INITIALIZED;
        if (data.length < 16) throw XPR.NS_ERROR_ILLEGAL_VALUE;
        var C = data.substring(0, data.length - 16);
        var T = blockxor(ghash(C), encipher(Y0));
        var mac = data.substring(data.length - 16);
        if (tostring([T]) != mac) throw XPR.NS_ERROR_ILLEGAL_VALUE;
        return this.transform(C);
    }

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

