//
// Class SBox
//

function SBox()
{
    this.sBoxArray_ = new Array(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);

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

    this.subByte = function(b)
    {
        return this.sBoxArray_[b];
    }

    this.inverseSubByte =  function(b)
    {
        return this.inverseSboxArray_[b];
    }
}

//
// Class KeySchedule
//

function KeySchedule(Nb, Nk, Nr, sBox)
{
    this.Nb_ = Nb;
    this.Nk_ = Nk;
    this.Nr_ = Nr;
    this.sBox_ = sBox;
    this.keyScheduleArray_ = new Array(4 * this.Nb_ * (this.Nr_ + 1));
    this.rconBytes_ = new Array(256);
    var x = 1;
    for (var n = 0; n < 256; n++)
    {
        this.rconBytes_[n] = x;
        x <<= 1;
        if ((0x100 & x) == 0x100)
        {
            x ^= 0x1b;
            x &= 0xff;
        }
    }

    this.getByte = function(word, wordByte)
    {
        return this.keyScheduleArray_[word * 4 + wordByte];
    }

    this.setByte = function(word, wordByte, value)
    {
        this.keyScheduleArray_[word * 4 + wordByte] = value;
    }

    this.rotWord = function(temp)
    {
        var t = temp[0];
        for (var j = 0; j < 3; j++)
        {
            temp[j] = temp[j + 1];
        }
        temp[3] = t;
    }

    this.xorRcon = function(i, temp)
    {
        temp[0] ^= this.rconBytes_[i - 1];
        temp[0] &= 0xff;
        for (var j = 1; j < 4; j++)
        {
            temp[j] ^= 0x00;
            temp[j] &= 0xff;
        }
    }
    
    this.subWord = function(temp)
    {
        for (var j = 0; j < 4; j++)
        {
            temp[j] = this.sBox_.subByte(temp[j]);
        }
    }
    
    this.setKey = function(key)
    {
        var i = 0;
        for (; i < this.Nk_; i++)
        {
            for (var b = 0; b < 4; b++)
            {
                this.setByte(i, b, key[4 * i + b]); 
            }
        }
        for (; i < this.Nb_ * (this.Nr_ + 1); i++)
        {
            var temp = new Array(4);
            for (var j = 0; j < 4; j++)
            {
                temp[j] = this.getByte(i - 1, j);
            }
            if (i % this.Nk_ == 0)
            {
                this.rotWord(temp);
                this.subWord(temp);
                this.xorRcon(i / this.Nk_, temp);
            }
            else if (this.Nk_ > 6 && i % this.Nk_ == 4)
            {
                this.subWord(temp);
            }
            for (var j = 0; j < 4; j++)
            {
                var b = this.getByte(i - this.Nk_, j);
                b ^= temp[j];
                b &= 0xff;
                this.setByte(i, j, b);
            }
        }
 //       this.dump();
    }

    this.getRoundByte = function(round, row, column)
    {
        return this.getByte(round * this.Nb_ + column, row);
    }

    this.dump = function()
    {
        for (var i = 0; i < this.Nb_ * (this.Nr_ + 1); i++)
        {
            var s = "0" + i.toString();
            s = s.substr(s.length - 2, s.length) + " ";
            for (var j = 0; j < 4; j++)
            {
                var b = "00" + this.getByte(i, j).toString(16);
                s += b.substr(b.length - 2, b.length);
            }
            document.writeln(s);
        }
    }
}

//
// Class State
//

function State(Nb, sBox)
{
    this.Nb_ = Nb;
    this.sBox_ = sBox;
    this.stateArray_ = new Array(4 * Nb);
    this.shiftPlaces_ = new Array(1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 4, 1, 3, 4);
    this.tempRow_ = new Array(this.Nb_);
    this.multiplyTable = new Array(16 * 256);
    for (var i = 0; i < 16; i++)
    {
        for (var j = 0; j < 256; j++)
        {
            var a = i;
            var b = j;
            var c = 0;
            while (b > 0)
            {
                if ((0x1 & b) == 0x1)
                {
                    c ^= a;
                    c &= 0xff;
                }
                b >>= 1;
                a <<= 1;
                if ((0x100 & a) == 0x100)
                {
                    a ^= 0x1b;
                    a &= 0xff;
                }
            }
            this.multiplyTable[i * 256 + j] = c;
        }
    }
    
    this.getByte = function(row, column)
    {
        return this.stateArray_[column * 4 + row];
    }

    this.setByte = function(row, column, value)
    {
        this.stateArray_[column * 4 + row] = value;
    }

    this.inputBytes = function(bytes)
    {
        for (var c = 0; c < this.Nb_; c++)
        {
            for (var r = 0; r < 4; r++)
            {
                this.setByte(r, c, bytes[c * 4 + r]);
            }
        }
    }

    this.outputBytes = function(bytes)
    {
        for (var c = 0; c < this.Nb_; c++)
        {
            for (var r = 0; r < 4; r++)
            {
                bytes[c * 4 + r] = this.getByte(r, c);
            }
        }
    }

    this.multiply = function(a, b)
    {
        return this.multiplyTable[a * 256 + b];
    }

    this.subBytes = function()
    {
        for (var c = 0; c < this.Nb_; c++)
        {
            for (var r = 0; r < 4; r++)
            {
                var b = this.getByte(r, c);
                b = this.sBox_.subByte(b);
                this.setByte(r, c, b);
                
            }
        }
    }
    
    this.inverseSubBytes = function()
    {
        for (var c = 0; c < this.Nb_; c++)
        {
            for (var r = 0; r < 4; r++)
            {
                var b = this.getByte(r, c);
                b = this.sBox_.inverseSubByte(b);
                this.setByte(r, c, b);
            }
        }
    }
    
    this.shiftRows = function()
    {
        for (var r = 1; r < 4; r++)
        {
            var shiftBy = this.shiftPlaces_[(this.Nb_ - 4) * 3 + r - 1];
            for (var c = 0; c < this.Nb_; c++)
            {
                this.tempRow_[c] = this.getByte(r, (c + shiftBy) % this.Nb_);
            }
            for (var c = 0; c < this.Nb_; c++)
            {
                this.setByte(r, c, this.tempRow_[c]);
            }
        }
    }

    this.inverseShiftRows = function()
    {
        for (var r = 1; r < 4; r++)
        {
            for (var c = 0; c < this.Nb_; c++)
            {
                this.tempRow_[c] = this.getByte(r, c);
            }
            var shiftBy = this.shiftPlaces_[(this.Nb_ - 4) * 3 + r - 1];
            for (var c = 0; c < this.Nb_; c++)
            {
                this.setByte(r, (c + shiftBy) % this.Nb_, this.tempRow_[c]);
            }
        }
    }

    this.mixColumns = function()
    {
        for (var c = 0; c < this.Nb_; c++)
        {
            var s0 = this.multiply(0x02, this.getByte(0, c))
                     ^ this.multiply(0x03,  this.getByte(1, c))
                     ^ this.getByte(2, c)
                     ^ this.getByte(3, c);
            var s1 = this.getByte(0, c)
                     ^ this.multiply(0x02, this.getByte(1, c))
                     ^ this.multiply(0x03,  this.getByte(2, c))
                     ^ this.getByte(3, c);
            var s2 = this.getByte(0, c)
                     ^ this.getByte(1, c)
                     ^ this.multiply(0x02, this.getByte(2, c))
                     ^ this.multiply(0x03,  this.getByte(3, c));
            var s3 = this.multiply(0x03, this.getByte(0, c))
                     ^ this.getByte(1, c)
                     ^ this.getByte(2, c)
                     ^ this.multiply(0x02,  this.getByte(3, c));
            s0 &= 0xff;
            s1 &= 0xff;
            s2 &= 0xff;
            s3 &= 0xff;
            this.setByte(0, c, s0);
            this.setByte(1, c, s1);
            this.setByte(2, c, s2);
            this.setByte(3, c, s3);
        }
    }

    this.inverseMixColumns = function()
    {
        for (var c = 0; c < this.Nb_; c++)
        {
            var s0 = this.multiply(0x0e, this.getByte(0, c))
                     ^ this.multiply(0x0b,  this.getByte(1, c))
                     ^ this.multiply(0x0d,  this.getByte(2, c))
                     ^ this.multiply(0x09,  this.getByte(3, c));
            var s1 = this.multiply(0x09, this.getByte(0, c))
                     ^ this.multiply(0x0e,  this.getByte(1, c))
                     ^ this.multiply(0x0b,  this.getByte(2, c))
                     ^ this.multiply(0x0d,  this.getByte(3, c));
            var s2 = this.multiply(0x0d, this.getByte(0, c))
                     ^ this.multiply(0x09,  this.getByte(1, c))
                     ^ this.multiply(0x0e,  this.getByte(2, c))
                     ^ this.multiply(0x0b,  this.getByte(3, c));
            var s3 = this.multiply(0x0b, this.getByte(0, c))
                     ^ this.multiply(0x0d,  this.getByte(1, c))
                     ^ this.multiply(0x09,  this.getByte(2, c))
                     ^ this.multiply(0x0e,  this.getByte(3, c));
            this.setByte(0, c, s0);
            this.setByte(1, c, s1);
            this.setByte(2, c, s2);
            this.setByte(3, c, s3);
        }
    }
    
    this.addRoundKey = function(round, keySchedule)
    {
        for (var c = 0; c < this.Nb_; c++)
        {
            for (var r = 0; r < 4; r++)
            {
                var b = this.getByte(r, c);
                b ^= keySchedule.getRoundByte(round, r, c);
                this.setByte(r, c, b);
            }
        }
    }

    this.dump = function()
    {
        var s = "";
        for (var c = 0; c < this.Nb_; c++)
        {
            for (var r = 0; r < 4; r++)
            {
                var b = "00" + this.getByte(r, c).toString(16);
                s += b.substr(b.length - 2, b.length);
            }
        }
        document.writeln(s);
    }
}

//
// Class AES
//

function AES(Nb, Nk)
{
    if (Nb < 4 || Nb > 8)
    {
        throw new Error("Invalid block size");
    }
    if (Nk < 4 || Nk > 8)
    {
        throw new Error("Invalid key size");
    }
    this.Nb_ = Nb;
    this.Nk_ = Nk;
    this.Nr_ = Math.max(Nb, Nk) + 6;
    this.sbox_ = new SBox();
    this.keySchedule_ = new KeySchedule(this.Nb_, this.Nk_, this.Nr_, this.sbox_);
    this.state_ = new State(this.Nb_, this.sbox_);
    this.block_ = new Array(4 * this.Nb_);
    this.currentBlock_ = new Array(4 * this.Nb_);
    this.lastBlock_ = new Array(4 * this.Nb_);
    this.tempBlock_ = new Array(4 * this.Nb_);

    this.getKeySize = function()
    {
        return this.Nk_ * 4;
    }
    
    this.getBlockSize = function()
    {
        return this.Nb_ * 4;
    }
    
    this.setKey = function(key)
    {
        if (key.length != 4 * this.Nk_)
        {
            throw new Error("Invalid key length");
        }
        var intKey = new Array(4 * this.Nk_);
        for (var i = 0; i < 4 * this.Nk_; i++)
        {
            intKey[i] = key[i] & 0xff;
        }
        this.keySchedule_.setKey(intKey);  
    }

    this.bytesToCurrentBlock = function(b)
    {
        for (var n = 0; n < 4 * this.Nb_; n++)
        {
            this.currentBlock_[n] = b[n] & 0xff;
        }
    }

    this.currentBlockToBytes = function(b)
    {
        for (var n = 0; n < 4 * this.Nb_; n++)
        {
            b[n] = this.currentBlock_[n] ;
        }
    }

    this.readBlock = function(input)
    {
        input.readBlock(this.block_);
        this.bytesToCurrentBlock(this.block_);
    }
    
    this.writeBlock = function(output)
    {
        this.currentBlockToBytes(this.block_);
        output.writeBlock(this.block_);
    }
    
    this.copyBlock = function(source, dest)
    {
        arrayCopy(source, 0, dest, 0, this.Nb_ * 4);
    }
    
    this.encryptCBC = function(iv, input, output)
    {
        input.setBlockSize(this.Nb_ * 4);
        output.setBlockSize(this.Nb_ * 4);
        this.bytesToCurrentBlock(iv);
        this.copyBlock(this.currentBlock_, this.lastBlock_);
        this.writeBlock(output);
        while (input.more())
        {
            this.readBlock(input);
            for (var n = 0; n < 4 * this.Nb_; n++)
            {
                this.currentBlock_[n] ^= this.lastBlock_[n];
                this.currentBlock_[n] &= 0xff;
            }
            this.encrypt();
            this.copyBlock(this.currentBlock_, this.lastBlock_);
            this.writeBlock(output);
        }
        output.done();
    }

    this.decryptCBC = function(input, output)
    {
        input.setBlockSize(this.Nb_ * 4);
        output.setBlockSize(this.Nb_ * 4);
        this.readBlock(input);
        this.copyBlock(this.currentBlock_, this.lastBlock_);
        while (input.more())
        {
            this.readBlock(input);
            this.copyBlock(this.currentBlock_, this.tempBlock_);
            this.decrypt();
            for (var n = 0; n < 4 * this.Nb_; n++)
            {
                this.currentBlock_[n] ^= this.lastBlock_[n];
                this.currentBlock_[n] &= 0xff;
            }
            this.writeBlock(output);
            this.copyBlock(this.tempBlock_, this.lastBlock_);
        }
        output.done();
    }
  
    this.encryptBlock = function(plainText, ciferText)
    {
        if (plainText.length != this.Nb_ * 4 || ciferText.length != this.Nb_ * 4)
        {
            throw new Error("Invalid block size");
        }
        this.bytesToCurrentBlock(plainText);
        this.encrypt();
        this.currentBlockToBytes(ciferText);
    }

    this.decryptBlock = function(ciferText, plainText)
    {
        if (plainText.length != this.Nb_ * 4 || ciferText.length != this.Nb_ * 4)
        {
            throw new Error("Invalid block size");
        }
        this.bytesToCurrentBlock(ciferText);
        this.decrypt();
        this.currentBlockToBytes(plainText);
    }

    this.encrypt = function()
    {
        this.state_.inputBytes(this.currentBlock_);
        this.state_.addRoundKey(0, this.keySchedule_);
        for (var round = 1; round < this.Nr_; round++)
        {
            this.state_.subBytes();
//            this.dump();
            this.state_.shiftRows();
            this.state_.mixColumns();
            this.state_.addRoundKey(round, this.keySchedule_);
        }
        this.state_.subBytes();
        this.state_.shiftRows();
        this.state_.addRoundKey(this.Nr_, this.keySchedule_);
        this.state_.outputBytes(this.currentBlock_);
    }
    
    this.decrypt = function()
    {
        this.state_.inputBytes(this.currentBlock_);
        this.state_.addRoundKey(this.Nr_, this.keySchedule_);
        for (var round = this.Nr_ - 1; round > 0; round--)
        {
            this.state_.inverseShiftRows();
            this.state_.inverseSubBytes();
            this.state_.addRoundKey(round, this.keySchedule_);
            this.state_.inverseMixColumns();
        }
        this.state_.inverseShiftRows();
        this.state_.inverseSubBytes();
        this.state_.addRoundKey(0, this.keySchedule_);
        this.state_.outputBytes(this.currentBlock_);
    }

    this.dump = function()
    {
        this.state_.dump();
    }
}

