162 lines
6.4 KiB
JavaScript
162 lines
6.4 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert'),
|
|
BoxError = require('./boxerror.js'),
|
|
crypto = require('crypto'),
|
|
debug = require('debug')('box:hush'),
|
|
TransformStream = require('stream').Transform;
|
|
|
|
class EncryptStream extends TransformStream {
|
|
constructor(encryption) {
|
|
super();
|
|
this._headerPushed = false;
|
|
this._iv = crypto.randomBytes(16);
|
|
this._cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(encryption.dataKey, 'hex'), this._iv);
|
|
this._hmac = crypto.createHmac('sha256', Buffer.from(encryption.dataHmacKey, 'hex'));
|
|
}
|
|
|
|
pushHeaderIfNeeded() {
|
|
if (!this._headerPushed) {
|
|
const magic = Buffer.from('CBV2');
|
|
this.push(magic);
|
|
this._hmac.update(magic);
|
|
this.push(this._iv);
|
|
this._hmac.update(this._iv);
|
|
this._headerPushed = true;
|
|
}
|
|
}
|
|
|
|
_transform(chunk, ignoredEncoding, callback) {
|
|
this.pushHeaderIfNeeded();
|
|
|
|
try {
|
|
const crypt = this._cipher.update(chunk);
|
|
this._hmac.update(crypt);
|
|
callback(null, crypt);
|
|
} catch (error) {
|
|
callback(new BoxError(BoxError.CRYPTO_ERROR, `Encryption error when updating: ${error.message}`));
|
|
}
|
|
}
|
|
|
|
_flush(callback) {
|
|
try {
|
|
this.pushHeaderIfNeeded(); // for 0-length files
|
|
const crypt = this._cipher.final();
|
|
this.push(crypt);
|
|
this._hmac.update(crypt);
|
|
callback(null, this._hmac.digest()); // +32 bytes
|
|
} catch (error) {
|
|
callback(new BoxError(BoxError.CRYPTO_ERROR, `Encryption error when flushing: ${error.message}`));
|
|
}
|
|
}
|
|
}
|
|
|
|
class DecryptStream extends TransformStream {
|
|
constructor(encryption) {
|
|
super();
|
|
this._key = Buffer.from(encryption.dataKey, 'hex');
|
|
this._header = Buffer.alloc(0);
|
|
this._decipher = null;
|
|
this._hmac = crypto.createHmac('sha256', Buffer.from(encryption.dataHmacKey, 'hex'));
|
|
this._buffer = Buffer.alloc(0);
|
|
}
|
|
|
|
_transform(chunk, ignoredEncoding, callback) {
|
|
assert(Buffer.isBuffer(chunk));
|
|
|
|
const needed = 20 - this._header.length; // 4 for magic, 16 for iv
|
|
|
|
if (this._header.length !== 20) { // not gotten header yet
|
|
this._header = Buffer.concat([this._header, chunk.subarray(0, needed)]);
|
|
if (this._header.length !== 20) return callback();
|
|
|
|
if (!this._header.subarray(0, 4).equals(new Buffer.from('CBV2'))) return callback(new BoxError(BoxError.CRYPTO_ERROR, 'Invalid magic in header'));
|
|
|
|
const iv = this._header.subarray(4);
|
|
this._decipher = crypto.createDecipheriv('aes-256-cbc', this._key, iv);
|
|
this._hmac.update(this._header);
|
|
}
|
|
|
|
this._buffer = Buffer.concat([ this._buffer, chunk.subarray(needed) ]);
|
|
if (this._buffer.length < 32) return callback(); // hmac trailer length is 32
|
|
|
|
try {
|
|
const cipherText = this._buffer.subarray(0, -32);
|
|
this._hmac.update(cipherText);
|
|
const plainText = this._decipher.update(cipherText);
|
|
this._buffer = this._buffer.subarray(-32);
|
|
callback(null, plainText);
|
|
} catch (error) {
|
|
callback(new BoxError(BoxError.CRYPTO_ERROR, `Decryption error: ${error.message}`));
|
|
}
|
|
}
|
|
|
|
_flush (callback) {
|
|
if (this._buffer.length !== 32) return callback(new BoxError(BoxError.CRYPTO_ERROR, 'Invalid password or tampered file (not enough data)'));
|
|
|
|
try {
|
|
if (!this._hmac.digest().equals(this._buffer)) return callback(new BoxError(BoxError.CRYPTO_ERROR, 'Invalid password or tampered file (mac mismatch)'));
|
|
|
|
const plainText = this._decipher.final();
|
|
callback(null, plainText);
|
|
} catch (error) {
|
|
callback(new BoxError(BoxError.CRYPTO_ERROR, `Invalid password or tampered file: ${error.message}`));
|
|
}
|
|
}
|
|
}
|
|
|
|
function encryptFilePath(filePath, encryption) {
|
|
assert.strictEqual(typeof filePath, 'string');
|
|
assert.strictEqual(typeof encryption, 'object');
|
|
|
|
const encryptedParts = filePath.split('/').map(function (part) {
|
|
const hmac = crypto.createHmac('sha256', Buffer.from(encryption.filenameHmacKey, 'hex'));
|
|
const iv = hmac.update(part).digest().subarray(0, 16); // iv has to be deterministic, for our sync (copy) logic to work
|
|
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(encryption.filenameKey, 'hex'), iv);
|
|
let crypt = cipher.update(part);
|
|
crypt = Buffer.concat([ iv, crypt, cipher.final() ]);
|
|
|
|
return crypt.toString('base64') // ensures path is valid
|
|
.replace(/\//g, '-') // replace '/' of base64 since it conflicts with path separator
|
|
.replace(/=/g,''); // strip trailing = padding. this is only needed if we concat base64 strings, which we don't
|
|
});
|
|
|
|
return encryptedParts.join('/');
|
|
}
|
|
|
|
function decryptFilePath(filePath, encryption) {
|
|
assert.strictEqual(typeof filePath, 'string');
|
|
assert.strictEqual(typeof encryption, 'object');
|
|
|
|
const decryptedParts = [];
|
|
for (let part of filePath.split('/')) {
|
|
part = part + Array(part.length % 4).join('='); // add back = padding
|
|
part = part.replace(/-/g, '/'); // replace with '/'
|
|
|
|
try {
|
|
const buffer = Buffer.from(part, 'base64');
|
|
const iv = buffer.subarray(0, 16);
|
|
const decrypt = crypto.createDecipheriv('aes-256-cbc', Buffer.from(encryption.filenameKey, 'hex'), iv);
|
|
const plainText = decrypt.update(buffer.subarray(16));
|
|
const plainTextString = Buffer.concat([ plainText, decrypt.final() ]).toString('utf8');
|
|
const hmac = crypto.createHmac('sha256', Buffer.from(encryption.filenameHmacKey, 'hex'));
|
|
if (!hmac.update(plainTextString).digest().subarray(0, 16).equals(iv)) return { error: new BoxError(BoxError.CRYPTO_ERROR, `mac error decrypting part ${part} of path ${filePath}`) };
|
|
|
|
decryptedParts.push(plainTextString);
|
|
} catch (error) {
|
|
debug(`Error decrypting part ${part} of path ${filePath}: %o`, error);
|
|
return { error: new BoxError(BoxError.CRYPTO_ERROR, `Error decrypting part ${part} of path ${filePath}: ${error.message}`) };
|
|
}
|
|
}
|
|
|
|
return { result: decryptedParts.join('/') };
|
|
}
|
|
|
|
exports = module.exports = {
|
|
EncryptStream,
|
|
DecryptStream,
|
|
|
|
encryptFilePath,
|
|
decryptFilePath,
|
|
};
|