//
// Block Input
//

function BlockInput(input)
{
    this.in_ = input;
    this.used_ = 0;
    

    this.setBlockSize = function(blockSize)
    {
    }

    this.more = function()
    {
        return this.used_ < this.in_.length;
    }

    this.readBlock = function(block)
    {
        if (this.in_.length - this.used_ < block.length)
        {
            throw new Error("Invalid block size");
        }
        arrayCopy(this.in_, this.used_, block, 0, block.length);
        this.used_ += block.length
    }

}

//
// Block Output
//

function BlockOutput()
{

    this.out_ = new Array(0);

    this.getOutput = function()
    {
        return this.out_;
    }

    this.setBlockSize = function(blockSize)
    {
    }
    
    this.done = function()
    {
    }

    this.writeBlock = function(block)
    {
        var length = this.out_.length;
        this.out_.length += block.length;
        arrayCopy(block, 0, this.out_, length, block.length);
    }
}


//
// PaddedBlockInput
//

function PaddedBlockInput(input)
{
    this.in_ = input;
    this.used_ = 0;
    this.blockSize_ = 0;
    

    this.setBlockSize = function(blockSize)
    {
        if (this.blockSize_ != 0)
        {
            throw new Error("Block size already set");
        }
        this.blockSize_ = blockSize;
        var length = this.in_.length;
        var pad = this.blockSize_ - (length % this.blockSize_);
        this.in_.length += pad;
        for (var i = length; i < this.in_.length; i++)
        {
            this.in_[i] = pad;
        }
    }

    this.more = function()
    {
        return this.used_ < this.in_.length;
    }

    this.readBlock = function(block)
    {
        if (this.blocksize_ == 0)
        {
            throw new Error("Block size not set");
        }
        if (this.in_.length - this.used_ < block.length)
        {
            throw new Error("Invalid block size");
        }
        arrayCopy(this.in_, this.used_, block, 0, block.length);
        this.used_ += block.length
    }
}

//
// Padded Block Output
//

function PaddedBlockOutput()
{

    this.out_ = new Array(0);
    this.blockSize_ = 0;

    this.getOutput = function()
    {
        return this.out_;
    }

    this.setBlockSize = function(blockSize)
    {
        if (this.blockSize_ != 0)
        {
            throw new Error("Block size already set");
        }
        this.blockSize_ = blockSize;
    }
    
    this.done = function()
    {
        var pad = this.out_[this.out_.length - 1];
        if (pad > this.blockSize_ || pad > this.out_.length)
        {
            throw new Error("Invalid pad size");
        }
        var n = pad;
        while (n-- > 0)
        {
            var p = this.out_.pop();
            if (pad != p)
            {
                throw new Error("Invalid paddding");
            }
        }
    }

    this.writeBlock = function(block)
    {
        var length = this.out_.length;
        this.out_.length += block.length;
        arrayCopy(block, 0, this.out_, length, block.length);
    }
}

//
// IVGenerator
//

function IVGenerator(cipher)
{
    
    this.longToBlock = function(l, block)
    {
        for (var n = 0; n < block.length && l > 0; n++)
        {
            block[n] = (l & 0xff);
            l >>>= 8;
        }
    }
    
    var key = (new Date()).getTime();
    this.cipher_ = cipher;
    this.iv_ = new Array(this.cipher_.getBlockSize());
    var keyBytes = new Array(this.cipher_.getKeySize());
    this.longToBlock(key, keyBytes);
    this.cipher_.setKey(keyBytes);
    this.countAsBytes_ = new Array(this.cipher_.getBlockSize());
    this.count_ = 0;

    this.nextIV = function()
    {
        this.longToBlock(this.count_++, this.countAsBytes_);
        this.cipher_.encryptBlock(this.countAsBytes_, this.iv_);
        return this.iv_;
    }
}

//
//  Block Cipher Hash
//

function BlockCipherHash(cipher)
{
    this.cipher_ = cipher; 
    this.key_ = new Array(this.cipher_.getKeySize());
    this.blockIn_ = new Array(this.cipher_.getBlockSize());
    this.blockOut_ = new Array(this.cipher_.getBlockSize());
    
    this.hashString = function(s)
    {
        var sBytes = stringToLatin1(s);
        return this.hash(sBytes);
    }

    this.hashBlock = function()
    {
        this.cipher_.setKey(this.key_);
        this.cipher_.encryptBlock(this.blockIn_, this.blockOut_);
        for (var j = 0; j < this.cipher_.getBlockSize(); j++)
        {
            this.blockIn_[j] ^= this.blockOut_[j];
        }
    }
    
    this.hash = function(input)
    {
        var i = 0;
        for (; i < input.length - this.cipher_.getKeySize(); i += this.cipher_.getKeySize())
        {
            arrayCopy(input, i, this.key_, 0, this.cipher_.getKeySize());
            this.hashBlock();
        }
        arrayCopy(input, i, this.key_, 0, input.length - i);
        for (var j = input.length - i; j < this.cipher_.getKeySize(); j++)
        {
            this.key_[j] = 0;
        }
        this.hashBlock();
        return this.blockOut_;
    }
}

//
//   String Cipher
//

function StringCipher(cipher, ivGenerator)
{
    this.cipher_ = cipher;
    this.ivGenerator_ = ivGenerator;
    this.blockSize_ = this.cipher_.getBlockSize();
    
    this.encryptString = function(plainText)
    {
        var plainTextBytes = stringToLatin1(plainText);
        var plainTextIn = new PaddedBlockInput(plainTextBytes);
        var cipherTextOut = new BlockOutput();
        this.cipher_.encryptCBC(this.ivGenerator_.nextIV(), plainTextIn, cipherTextOut);
        return bytesToHex(cipherTextOut.getOutput());
    }

    this.decryptString = function(cipherText)
    {
        var cipherTextBytes = hexToBytes(cipherText);
        var cipherTextIn = new BlockInput(cipherTextBytes);
        var plainTextOut = new PaddedBlockOutput();
        this.cipher_.decryptCBC(cipherTextIn, plainTextOut);
        return latin1ToString(plainTextOut.getOutput());
    }
}

