mostly because code is being autogenerated by all the AI stuff using this prefix. it's also used in the stack trace.
177 lines
7.1 KiB
JavaScript
177 lines
7.1 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert'),
|
|
BoxError = require('./boxerror.js'),
|
|
crypto = require('node:crypto'),
|
|
debug = require('debug')('box:hush'),
|
|
TransformStream = require('node: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, 'Decryption 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, 'Decryption error. Invalid password or tampered file (not enough data)'));
|
|
|
|
try {
|
|
if (!this._hmac.digest().equals(this._buffer)) return callback(new BoxError(BoxError.CRYPTO_ERROR, 'Decryption error. Invalid password or tampered file (mac mismatch)'));
|
|
|
|
const plainText = this._decipher.final();
|
|
callback(null, plainText);
|
|
} catch (error) {
|
|
callback(new BoxError(BoxError.CRYPTO_ERROR, `Decryption 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, `Decryption error. HMAC 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, `Decryption error. ${part} of path ${filePath}: ${error.message}`) };
|
|
}
|
|
}
|
|
|
|
return { result: decryptedParts.join('/') };
|
|
}
|
|
|
|
// this function is used in migrations - 20200512172301-settings-backup-encryption.js
|
|
function generateEncryptionKeysSync(password) {
|
|
assert.strictEqual(typeof password, 'string');
|
|
|
|
const aesKeys = crypto.scryptSync(password, Buffer.from('CLOUDRONSCRYPTSALT', 'utf8'), 128);
|
|
return {
|
|
dataKey: aesKeys.subarray(0, 32).toString('hex'),
|
|
dataHmacKey: aesKeys.subarray(32, 64).toString('hex'),
|
|
filenameKey: aesKeys.subarray(64, 96).toString('hex'),
|
|
filenameHmacKey: aesKeys.subarray(96).toString('hex')
|
|
};
|
|
}
|
|
|
|
exports = module.exports = {
|
|
EncryptStream,
|
|
DecryptStream,
|
|
|
|
encryptFilePath,
|
|
decryptFilePath,
|
|
|
|
generateEncryptionKeysSync,
|
|
};
|