node-jose-browserify
Version:
An advanced version of Cisco's node-jose module that works in both browser and the server.
308 lines (254 loc) • 7.79 kB
JavaScript
/*!
* deps/ciphermodes/gcm/index.js - AES-GCM implementation Entry Point
*
* Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file.
*/
"use strict";
var Long = require("long"),
forge = require("../../../deps/forge.js"),
multipliers = require("./multipliers.js"),
helpers = require("./helpers.js"),
pack = require("../pack.js"),
DataBuffer = require("../../../util/databuffer.js"),
cipherHelpers = require("../helpers.js");
var BLOCK_SIZE = 16;
// ### GCM Mode
// ### Constructor
function Gcm(options) {
options = options || {};
this.name = "GCM";
this.cipher = options.cipher;
this.blockSize = this.blockSize || 16;
}
// ### exports
module.exports = {
createCipher: function(options) {
var alg = new forge.aes.Algorithm("AES-GCM", Gcm);
alg.initialize({
key: new DataBuffer(options.key)
});
alg.mode.start(options);
return alg.mode;
},
createDecipher: function(options) {
var alg = new forge.aes.Algorithm("AES-GCM", Gcm);
alg.initialize({
key: new DataBuffer(options.key)
});
alg.mode._decrypt = true;
alg.mode.start(options);
return alg.mode;
}
};
// ### Public API
Gcm.prototype.start = function(options) {
this.tag = null;
options = options || {};
if (!("iv" in options)) {
throw new Error("Gcm needs ParametersWithIV or AEADParameters");
}
this.nonce = options.iv;
if (this.nonce == null || this.nonce.length < 1) {
throw new Error("IV must be at least 1 byte");
}
// TODO: variable tagLength?
this.tagLength = 16;
// TODO: validate tag
if ("tag" in options) {
this.tag = Buffer.from(options.tag);
}
var bufLength = !this._decrypt ?
this.blockSize :
(this.blockSize + this.tagLength);
this.bufBlock = Buffer.alloc(bufLength);
var multiplier = options.multiplier;
if (multiplier == null) {
multiplier = new (multipliers["8k"])();
}
this.multiplier = multiplier;
this.H = this.zeroBlock();
cipherHelpers.encrypt(this.cipher, this.H, 0, this.H, 0);
// GcmMultiplier tables don"t change unless the key changes
// (and are expensive to init)
this.multiplier.init(this.H);
this.exp = null;
this.J0 = this.zeroBlock();
if (this.nonce.length === 12) {
this.nonce.copy(this.J0, 0, 0, this.nonce.length);
this.J0[this.blockSize - 1] = 0x01;
} else {
this.gHASH(this.J0, this.nonce, this.nonce.length);
var X = this.zeroBlock();
pack.longToBigEndian(new Long(this.nonce.length).
multiply(8), X, 8);
this.gHASHBlock(this.J0, X);
}
this.S = this.zeroBlock();
this.SAt = this.zeroBlock();
this.SAtPre = this.zeroBlock();
this.atBlock = this.zeroBlock();
this.atBlockPos = 0;
this.atLength = Long.ZERO;
this.atLengthPre = Long.ZERO;
this.counter = Buffer.from(this.J0);
this.bufOff = 0;
this.totalLength = Long.ZERO;
if ("additionalData" in options) {
this.processAADBytes(options.additionalData, 0, options.additionalData.length);
}
};
Gcm.prototype.update = function(inV, inOff, len, out, outOff) {
var resultLen = 0;
while (len > 0) {
var inLen = Math.min(len, this.bufBlock.length - this.bufOff);
inV.copy(this.bufBlock, this.bufOff, inOff, inOff + inLen);
len -= inLen;
inOff += inLen;
this.bufOff += inLen;
if (this.bufOff === this.bufBlock.length) {
this.outputBlock(out, outOff + resultLen);
resultLen += this.blockSize;
}
}
return resultLen;
};
Gcm.prototype.finish = function(out, outOff) {
var resultLen = 0;
if (this._decrypt) {
// append tag
resultLen += this.update(this.tag, 0, this.tag.length, out, outOff);
}
if (this.totalLength.isZero()) {
this.initCipher();
}
var extra = this.bufOff;
if (this._decrypt) {
if (extra < this.tagLength) {
throw new Error("data too short");
}
extra -= this.tagLength;
}
if (extra > 0) {
this.gCTRPartial(this.bufBlock, 0, extra, out, outOff + resultLen);
resultLen += extra;
}
this.atLength = this.atLength.add(this.atBlockPos);
// Final gHASH
var X = this.zeroBlock();
pack.longToBigEndian(this.atLength.multiply(8),
X,
0);
pack.longToBigEndian(this.totalLength.multiply(8),
X,
8);
this.gHASHBlock(this.S, X);
// TODO Fix this if tagLength becomes configurable
// T = MSBt(GCTRk(J0,S))
var tag = Buffer.alloc(this.blockSize);
cipherHelpers.encrypt(this.cipher, this.J0, 0, tag, 0);
this.xor(tag, this.S);
if (this._decrypt) {
if (!helpers.arrayEqual(this.tag, tag)) {
throw new Error("mac check in Gcm failed");
}
} else {
// We place into tag our calculated value for T
this.tag = Buffer.alloc(this.tagLength);
tag.copy(this.tag, 0, 0, this.tagLength);
}
return resultLen;
};
// ### "Internal" Helper Functions
Gcm.prototype.initCipher = function() {
if (this.atLength.greaterThan(Long.ZERO)) {
this.SAt.copy(this.SAtPre, 0, 0, this.blockSize);
this.atLengthPre = this.atLength.add(Long.ZERO);
}
// Finish hash for partial AAD block
if (this.atBlockPos > 0) {
this.gHASHPartial(this.SAtPre, this.atBlock, 0, this.atBlockPos);
this.atLengthPre = this.atLengthPre.add(this.atBlockPos);
}
if (this.atLengthPre.greaterThan(Long.ZERO)) {
this.SAtPre.copy(this.S, 0, 0, this.blockSize);
}
};
Gcm.prototype.outputBlock = function(output, offset) {
if (this.totalLength.isZero()) {
this.initCipher();
}
this.gCTRBlock(this.bufBlock, output, offset);
if (!this._decrypt) {
this.bufOff = 0;
} else {
this.bufBlock.copy(this.bufBlock, 0, this.blockSize, this.blockSize + this.tagLength);
this.bufOff = this.tagLength;
}
};
Gcm.prototype.processAADBytes = function(inV, inOff, len) {
for (var i = 0; i < len; ++i) {
this.atBlock[this.atBlockPos] = inV[inOff + i];
if (++this.atBlockPos === this.blockSize) {
// Hash each block as it fills
this.gHASHBlock(this.SAt, this.atBlock);
this.atBlockPos = 0;
this.atLength = this.atLength.add(this.blockSize);
}
}
};
Gcm.prototype.getNextCounterBlock = function() {
for (var i = 15; i >= 12; --i) {
var b = ((this.counter[i] + 1) & 0xff);
this.counter[i] = b;
if (b !== 0) {
break;
}
}
// encrypt counter
var outb = Buffer.alloc(this.blockSize);
cipherHelpers.encrypt(this.cipher, this.counter, 0, outb, 0);
return outb;
};
Gcm.prototype.gCTRBlock = function(block, out, outOff) {
var tmp = this.getNextCounterBlock();
this.xor(tmp, block);
tmp.copy(out, outOff, 0, this.blockSize);
this.gHASHBlock(this.S, !this._decrypt ? tmp : block);
this.totalLength = this.totalLength.add(this.blockSize);
};
Gcm.prototype.gCTRPartial = function(buf, off, len, out, outOff) {
var tmp = this.getNextCounterBlock();
this.xor(tmp, buf, off, len);
tmp.copy(out, outOff, 0, len);
this.gHASHPartial(this.S, !this._decrypt ? tmp : buf, 0, len);
this.totalLength = this.totalLength.add(len);
};
Gcm.prototype.gHASHBlock = function(Y, b) {
this.xor(Y, b);
this.multiplier.multiplyH(Y);
};
Gcm.prototype.gHASHPartial = function(Y, b, off, len) {
this.xor(Y, b, off, len);
this.multiplier.multiplyH(Y);
};
Gcm.prototype.xor = function(block, val, off, len) {
switch (arguments.length) {
case 2:
for (var i = 15; i >= 0; --i) {
block[i] ^= val[i];
}
break;
case 4:
while (len-- > 0) {
block[len] ^= val[off + len];
}
break;
default:
throw new TypeError("Expected 2 or 4 arguments.");
}
return block;
};
Gcm.prototype.zeroBlock = function() {
var block = Buffer.alloc(BLOCK_SIZE);
return block;
};