2022-04-28 18:10:58 -07:00
'use strict' ;
const assert = require ( 'assert' ) ,
BoxError = require ( './boxerror.js' ) ,
crypto = require ( 'crypto' ) ,
debug = require ( 'debug' ) ( 'box:hush' ) ,
fs = require ( 'fs' ) ,
2022-11-06 10:17:14 +01:00
ProgressStream = require ( './progress-stream.js' ) ,
2022-04-28 18:10:58 -07:00
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 ) {
2022-04-29 19:02:59 -07:00
callback ( new BoxError ( BoxError . CRYPTO _ERROR , ` Encryption error when updating: ${ error . message } ` ) ) ;
2022-04-28 18:10:58 -07:00
}
}
_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 ) {
2022-04-29 19:02:59 -07:00
callback ( new BoxError ( BoxError . CRYPTO _ERROR , ` Encryption error when flushing: ${ error . message } ` ) ) ;
2022-04-28 18:10:58 -07:00
}
}
}
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 ) {
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 . slice ( 0 , needed ) ] ) ;
if ( this . _header . length !== 20 ) return callback ( ) ;
if ( ! this . _header . slice ( 0 , 4 ) . equals ( new Buffer . from ( 'CBV2' ) ) ) return callback ( new BoxError ( BoxError . CRYPTO _ERROR , 'Invalid magic in header' ) ) ;
const iv = this . _header . slice ( 4 ) ;
this . _decipher = crypto . createDecipheriv ( 'aes-256-cbc' , this . _key , iv ) ;
this . _hmac . update ( this . _header ) ;
}
this . _buffer = Buffer . concat ( [ this . _buffer , chunk . slice ( needed ) ] ) ;
if ( this . _buffer . length < 32 ) return callback ( ) ; // hmac trailer length is 32
try {
const cipherText = this . _buffer . slice ( 0 , - 32 ) ;
this . _hmac . update ( cipherText ) ;
const plainText = this . _decipher . update ( cipherText ) ;
this . _buffer = this . _buffer . slice ( - 32 ) ;
callback ( null , plainText ) ;
} catch ( error ) {
2022-04-29 19:02:59 -07:00
callback ( new BoxError ( BoxError . CRYPTO _ERROR , ` Decryption error: ${ error . message } ` ) ) ;
2022-04-28 18:10:58 -07:00
}
}
_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 ) {
2022-04-29 19:02:59 -07:00
callback ( new BoxError ( BoxError . CRYPTO _ERROR , ` Invalid password or tampered file: ${ error . message } ` ) ) ;
2022-04-28 18:10:58 -07:00
}
}
}
function encryptFilePath ( filePath , encryption ) {
assert . strictEqual ( typeof filePath , 'string' ) ;
assert . strictEqual ( typeof encryption , 'object' ) ;
const encryptedParts = filePath . split ( '/' ) . map ( function ( part ) {
let hmac = crypto . createHmac ( 'sha256' , Buffer . from ( encryption . filenameHmacKey , 'hex' ) ) ;
const iv = hmac . update ( part ) . digest ( ) . slice ( 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 . slice ( 0 , 16 ) ;
let decrypt = crypto . createDecipheriv ( 'aes-256-cbc' , Buffer . from ( encryption . filenameKey , 'hex' ) , iv ) ;
const plainText = decrypt . update ( buffer . slice ( 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 ( ) . slice ( 0 , 16 ) . equals ( iv ) ) return { error : new BoxError ( BoxError . CRYPTO _ERROR , ` mac error decrypting part ${ part } of path ${ filePath } ` ) } ;
decryptedParts . push ( plainTextString ) ;
} catch ( error ) {
2023-04-16 10:49:59 +02:00
debug ( ` Error decrypting part ${ part } of path ${ filePath } : %o ` , error ) ;
2022-04-28 18:10:58 -07:00
return { error : new BoxError ( BoxError . CRYPTO _ERROR , ` Error decrypting part ${ part } of path ${ filePath } : ${ error . message } ` ) } ;
}
}
return { result : decryptedParts . join ( '/' ) } ;
}
function createReadStream ( sourceFile , encryption ) {
assert . strictEqual ( typeof sourceFile , 'string' ) ;
assert . strictEqual ( typeof encryption , 'object' ) ;
const stream = fs . createReadStream ( sourceFile ) ;
2022-11-06 10:17:14 +01:00
const ps = new ProgressStream ( { interval : 10000 } ) ; // display a progress every 10 seconds
2022-04-28 18:10:58 -07:00
stream . on ( 'error' , function ( error ) {
2023-04-16 10:49:59 +02:00
debug ( ` createReadStream: read stream error at ${ sourceFile } . %o ` , error ) ;
2022-04-28 18:10:58 -07:00
ps . emit ( 'error' , new BoxError ( BoxError . FS _ERROR , ` Error reading ${ sourceFile } : ${ error . message } ${ error . code } ` ) ) ;
} ) ;
stream . on ( 'open' , ( ) => ps . emit ( 'open' ) ) ;
if ( encryption ) {
let encryptStream = new EncryptStream ( encryption ) ;
encryptStream . on ( 'error' , function ( error ) {
2023-04-16 10:49:59 +02:00
debug ( ` createReadStream: encrypt stream error ${ sourceFile } . %o ` , error ) ;
2022-04-28 18:10:58 -07:00
ps . emit ( 'error' , new BoxError ( BoxError . CRYPTO _ERROR , ` Encryption error at ${ sourceFile } : ${ error . message } ` ) ) ;
} ) ;
return stream . pipe ( encryptStream ) . pipe ( ps ) ;
} else {
return stream . pipe ( ps ) ;
}
}
2022-04-28 18:43:14 -07:00
exports = module . exports = {
EncryptStream ,
DecryptStream ,
encryptFilePath ,
decryptFilePath ,
createReadStream ,
} ;