Skip to content

Add support for AES encryption & decryption. #656

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .jshintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
lib/ctrGladman.js
3 changes: 3 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ module.exports = function(grunt) {
},
builtins: false
},
transform: [
'browserify-shim'
],
banner: grunt.file.read('lib/license_header.js').replace(/__VERSION__/, version)
}
}
Expand Down
43 changes: 32 additions & 11 deletions lib/compressedObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var DataWorker = require('./stream/DataWorker');
var DataLengthProbe = require('./stream/DataLengthProbe');
var Crc32Probe = require('./stream/Crc32Probe');
var DataLengthProbe = require('./stream/DataLengthProbe');
var AesWorker = require('./stream/AesWorker');

/**
* Represent a compressed object, with everything needed to decompress it.
Expand All @@ -15,12 +16,13 @@ var DataLengthProbe = require('./stream/DataLengthProbe');
* @param {object} compression the type of compression, see lib/compressions.js.
* @param {String|ArrayBuffer|Uint8Array|Buffer} data the compressed data.
*/
function CompressedObject(compressedSize, uncompressedSize, crc32, compression, data) {
function CompressedObject(compressedSize, uncompressedSize, crc32, compression, data, decryptOptions) {
this.compressedSize = compressedSize;
this.uncompressedSize = uncompressedSize;
this.crc32 = crc32;
this.compression = compression;
this.compressedContent = data;
this.decryptOptions = decryptOptions;
}

CompressedObject.prototype = {
Expand All @@ -29,9 +31,18 @@ CompressedObject.prototype = {
* @return {GenericWorker} the worker.
*/
getContentWorker : function () {
var worker = new DataWorker(external.Promise.resolve(this.compressedContent))
.pipe(this.compression.uncompressWorker())
.pipe(new DataLengthProbe("data_length"));
var worker;
if (this.decryptOptions) {
worker = new DataWorker(external.Promise.resolve(this.compressedContent))
.pipe(AesWorker.decryptWorker(this.decryptOptions))
.pipe(this.compression.uncompressWorker())
.pipe(new DataLengthProbe("data_length"));
} else {
worker = new DataWorker(external.Promise.resolve(this.compressedContent))
.pipe(this.compression.uncompressWorker())
.pipe(new DataLengthProbe("data_length"));
}


var that = this;
worker.on("end", function () {
Expand Down Expand Up @@ -63,13 +74,23 @@ CompressedObject.prototype = {
* @param {Object} compressionOptions the options to use when compressing.
* @return {GenericWorker} the new worker compressing the content.
*/
CompressedObject.createWorkerFrom = function (uncompressedWorker, compression, compressionOptions) {
return uncompressedWorker
.pipe(new Crc32Probe())
.pipe(new DataLengthProbe("uncompressedSize"))
.pipe(compression.compressWorker(compressionOptions))
.pipe(new DataLengthProbe("compressedSize"))
.withStreamInfo("compression", compression);
CompressedObject.createWorkerFrom = function (uncompressedWorker, compression, compressionOptions, encryptOptions) {
if (encryptOptions.password !== null) {
return uncompressedWorker
.pipe(new Crc32Probe())
.pipe(new DataLengthProbe("uncompressedSize"))
.pipe(compression.compressWorker(compressionOptions))
.pipe(AesWorker.encryptWorker(encryptOptions))
.pipe(new DataLengthProbe("compressedSize"))
.withStreamInfo("compression", compression);
} else {
return uncompressedWorker
.pipe(new Crc32Probe())
.pipe(new DataLengthProbe("uncompressedSize"))
.pipe(compression.compressWorker(compressionOptions))
.pipe(new DataLengthProbe("compressedSize"))
.withStreamInfo("compression", compression);
}
};

module.exports = CompressedObject;
110 changes: 110 additions & 0 deletions lib/ctrGladman.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/** @fileOverview CTR mode implementation.
*
* Special thanks to Roy Nicholson for pointing out a bug in our
* implementation.
*
* @author Emily Stark
* @author Mike Hamburg
* @author Dan Boneh
*/

/**
* CTR mode with CBC MAC.
* @namespace
*/
sjcl.mode.ctrGladman = {
/** The name of the mode.
* @constant
*/
name: "ctrGladman",

/** Encrypt in CTR mode.
* @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes.
* @param {bitArray} plaintext The plaintext data.
* @param {bitArray} iv The initialization value. It must be 128 bits.
* @param {bitArray} [adata=[]] The authenticated data. Must be empty.
* @return The encrypted data, an array of bytes.
* @throws {sjcl.exception.invalid} if the IV isn't exactly 128 bits or if any adata is specified.
*/
encrypt: function (prf, plaintext, iv, adata) {
return sjcl.mode.ctrGladman._calculate(prf, plaintext, iv, adata);
},

/** Decrypt in CTR mode.
* @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes.
* @param {bitArray} ciphertext The ciphertext data.
* @param {bitArray} iv The initialization value. It must be 128 bits.
* @param {bitArray} [adata=[]] The authenticated data. It must be empty.
* @return The decrypted data, an array of bytes.
* @throws {sjcl.exception.invalid} if the IV isn't exactly 128 bits or if any adata is specified.
* @throws {sjcl.exception.corrupt} if if the message is corrupt.
*/
decrypt: function (prf, ciphertext, iv, adata) {
return sjcl.mode.ctrGladman._calculate(prf, ciphertext, iv, adata);
},

incWord: function (word) {
if (((word >> 24) & 0xff) === 0xff) { //overflow
var b1 = (word >> 16) & 0xff;
var b2 = (word >> 8) & 0xff;
var b3 = word & 0xff;

if (b1 === 0xff) { // overflow b1
b1 = 0;
if (b2 === 0xff) {
b2 = 0;
if (b3 === 0xff) {
b3 = 0;
} else {
++b3;
}
} else {
++b2;
}
} else {
++b1;
}

word = 0;
word += (b1 << 16);
word += (b2 << 8);
word += b3;
} else {
word += (0x01 << 24);
}
return word;
},

incCounter: function (counter) {
if ((counter[0] = this.incWord(counter[0])) === 0) {
// encr_data in fileenc.c from Dr Brian Gladman's counts only with DWORD j < 8
counter[1] = this.incWord(counter[1]);
}
return counter;
},

_calculate: function (prf, data, iv, adata) {
var l, bl, res, c, d, e, i;
if (adata && adata.length) {
throw new sjcl.exception.invalid("ctr can't authenticate data");
}
if (sjcl.bitArray.bitLength(iv) !== 128) {
throw new sjcl.exception.invalid("ctr iv must be 128 bits");
}
if (!(l = data.length)) {
return [];
}
c = iv.slice(0);
d = data.slice(0);
bl = sjcl.bitArray.bitLength(d);
for (i = 0; i < l; i += 4) {
this.incCounter(c);
e = prf.encrypt(c);
d[i] ^= e[0];
d[i + 1] ^= e[1];
d[i + 2] ^= e[2];
d[i + 3] ^= e[3];
}
return sjcl.bitArray.clamp(d, bl);
}
};
40 changes: 31 additions & 9 deletions lib/generate/ZipFileWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ var generateDosExternalFileAttr = function (dosPermissions, isDir) {
* @param {Function} encodeFileName the function to encode the file name / comment.
* @return {Object} the zip parts.
*/
var generateZipParts = function(streamInfo, streamedContent, streamingEnded, offset, platform, encodeFileName) {
var generateZipParts = function(streamInfo, streamedContent, streamingEnded, offset, platform, encodeFileName, encryptOptions) {
var file = streamInfo['file'],
compression = streamInfo['compression'],
useCustomEncoding = encodeFileName !== utf8.utf8encode,
Expand All @@ -95,7 +95,8 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off
unicodePathExtraField = "",
unicodeCommentExtraField = "",
dir = file.dir,
date = file.date;
date = file.date,
isEncrypt = encryptOptions.password !== null;


var dataInfo = {
Expand All @@ -113,6 +114,9 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off
}

var bitflag = 0;
if (isEncrypt) {
bitflag |= 0x0001;
}
if (streamedContent) {
// Bit 3: the sizes/crc32 are set to zero in the local header.
// The correct values are put in the data descriptor immediately
Expand Down Expand Up @@ -183,7 +187,7 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off
unicodePathExtraField;
}

if(useUTF8ForComment) {
if (useUTF8ForComment) {

unicodeCommentExtraField =
// Version
Expand All @@ -202,14 +206,30 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off
unicodeCommentExtraField;
}

if (isEncrypt) {
extraFields += "\x01\x99";
extraFields += "\x07\x00";
extraFields += "\x02\x00";
extraFields += "AE";
extraFields += String.fromCharCode(encryptOptions.strength);
extraFields += compression.magic;
}
var header = "";

// version needed to extract
header += "\x0A\x00";
if (isEncrypt) {
header += "\x33\x00";
} else {
header += "\x0A\x00";
}
// general purpose bit flag
header += decToHex(bitflag, 2);
// compression method
header += compression.magic;
if (isEncrypt) {
header += "\x63\x00";
} else {
header += compression.magic;
}
// last mod file time
header += decToHex(dosTime, 2);
// last mod file date
Expand Down Expand Up @@ -319,7 +339,7 @@ var generateDataDescriptors = function (streamInfo) {
* @param {String} platform the platform to use, "UNIX" or "DOS".
* @param {Function} encodeFileName the function to encode file names and comments.
*/
function ZipFileWorker(streamFiles, comment, platform, encodeFileName) {
function ZipFileWorker(streamFiles, comment, platform, encodeFileName, encryptOptions) {
GenericWorker.call(this, "ZipFileWorker");
// The number of bytes written so far. This doesn't count accumulated chunks.
this.bytesWritten = 0;
Expand Down Expand Up @@ -348,6 +368,8 @@ function ZipFileWorker(streamFiles, comment, platform, encodeFileName) {
// Used for the emitted metadata.
this.currentFile = null;

this.encryptOptions = encryptOptions;



this._sources = [];
Expand Down Expand Up @@ -390,7 +412,7 @@ ZipFileWorker.prototype.openedSource = function (streamInfo) {

// don't stream folders (because they don't have any content)
if(streamedContent) {
var record = generateZipParts(streamInfo, streamedContent, false, this.currentSourceOffset, this.zipPlatform, this.encodeFileName);
var record = generateZipParts(streamInfo, streamedContent, false, this.currentSourceOffset, this.zipPlatform, this.encodeFileName, this.encryptOptions);
this.push({
data : record.fileRecord,
meta : {percent:0}
Expand All @@ -408,10 +430,10 @@ ZipFileWorker.prototype.openedSource = function (streamInfo) {
ZipFileWorker.prototype.closedSource = function (streamInfo) {
this.accumulate = false;
var streamedContent = this.streamFiles && !streamInfo['file'].dir;
var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName);
var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName, this.encryptOptions);

this.dirRecords.push(record.dirRecord);
if(streamedContent) {
if (streamedContent) {
// after the streamed file, we put data descriptors
this.push({
data : generateDataDescriptors(streamInfo),
Expand Down
12 changes: 9 additions & 3 deletions lib/generate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ var getCompression = function (fileCompression, zipCompression) {
* @param {String} comment the comment to use.
*/
exports.generateWorker = function (zip, options, comment) {

var zipFileWorker = new ZipFileWorker(options.streamFiles, comment, options.platform, options.encodeFileName);
var encryptOptions = {
password: options.password,
strength: options.encryptStrength
};
if (encryptOptions.password && typeof encryptOptions.password !== "string") {
throw new Error("Password is not a valid string.");
}
var zipFileWorker = new ZipFileWorker(options.streamFiles, comment, options.platform, options.encodeFileName, encryptOptions);
var entriesCount = 0;
try {

Expand All @@ -37,7 +43,7 @@ exports.generateWorker = function (zip, options, comment) {
var compressionOptions = file.options.compressionOptions || options.compressionOptions || {};
var dir = file.dir, date = file.date;

file._compressWorker(compression, compressionOptions)
file._compressWorker(compression, compressionOptions, encryptOptions)
.withStreamInfo("file", {
name : relativePath,
dir : dir,
Expand Down
1 change: 1 addition & 0 deletions lib/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function checkEntryCRC32(zipEntry) {
module.exports = function(data, options) {
var zip = this;
options = utils.extend(options || {}, {
password: null,
base64: false,
checkCRC32: false,
optimizedBinaryString: false,
Expand Down
2 changes: 2 additions & 0 deletions lib/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ var out = {
streamFiles: false,
compression: "STORE",
compressionOptions : null,
password: null,
encryptStrength: 3,
type: "",
platform: "DOS",
comment: null,
Expand Down
Loading