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' ) ,
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 ) {
2024-07-04 14:01:22 +02:00
assert ( Buffer . isBuffer ( chunk ) ) ;
2022-04-28 18:10:58 -07:00
const needed = 20 - this . _header . length ; // 4 for magic, 16 for iv
if ( this . _header . length !== 20 ) { // not gotten header yet
2024-07-04 14:01:22 +02:00
this . _header = Buffer . concat ( [ this . _header , chunk . subarray ( 0 , needed ) ] ) ;
2022-04-28 18:10:58 -07:00
if ( this . _header . length !== 20 ) return callback ( ) ;
2024-07-04 14:01:22 +02:00
if ( ! this . _header . subarray ( 0 , 4 ) . equals ( new Buffer . from ( 'CBV2' ) ) ) return callback ( new BoxError ( BoxError . CRYPTO _ERROR , 'Invalid magic in header' ) ) ;
2022-04-28 18:10:58 -07:00
2024-07-04 14:01:22 +02:00
const iv = this . _header . subarray ( 4 ) ;
2022-04-28 18:10:58 -07:00
this . _decipher = crypto . createDecipheriv ( 'aes-256-cbc' , this . _key , iv ) ;
this . _hmac . update ( this . _header ) ;
}
2024-07-04 14:01:22 +02:00
this . _buffer = Buffer . concat ( [ this . _buffer , chunk . subarray ( needed ) ] ) ;
2022-04-28 18:10:58 -07:00
if ( this . _buffer . length < 32 ) return callback ( ) ; // hmac trailer length is 32
try {
2024-07-04 14:01:22 +02:00
const cipherText = this . _buffer . subarray ( 0 , - 32 ) ;
2022-04-28 18:10:58 -07:00
this . _hmac . update ( cipherText ) ;
const plainText = this . _decipher . update ( cipherText ) ;
2024-07-04 14:01:22 +02:00
this . _buffer = this . _buffer . subarray ( - 32 ) ;
2022-04-28 18:10:58 -07:00
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 ) {
2024-07-04 14:01:22 +02:00
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
2022-04-28 18:10:58 -07:00
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' ) ;
2024-07-04 14:01:22 +02:00
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 ) ) ;
2022-04-28 18:10:58 -07:00
const plainTextString = Buffer . concat ( [ plainText , decrypt . final ( ) ] ) . toString ( 'utf8' ) ;
const hmac = crypto . createHmac ( 'sha256' , Buffer . from ( encryption . filenameHmacKey , 'hex' ) ) ;
2024-07-04 14:01:22 +02:00
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 } ` ) } ;
2022-04-28 18:10:58 -07:00
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 ( '/' ) } ;
}
2022-04-28 18:43:14 -07:00
exports = module . exports = {
EncryptStream ,
DecryptStream ,
encryptFilePath ,
decryptFilePath ,
} ;