UNPKG

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
/*! * 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; };