import angular from 'angular' ; import { Injectable } from 'angular-ts-decorators' ; import autobind from 'autobind-decorator' ; import base64js from 'base64-js' ; import lzutf8 from 'lzutf8' ; import { ArgumentError , ClientDataNotFoundError , InvalidCredentialsError } from '../errors/errors' ; import { LogService } from '../log/log.service' ; import { StoreKey } from '../store/store.enum' ; import { StoreService } from '../store/store.service' ; import { UtilityService } from '../utility/utility.service' ; @ autobind @ Injectable ( 'CryptoService' ) export class CryptoService { $q : ng . IQService ; logSvc : LogService ; storeSvc : StoreService ; utilitySvc : UtilityService ; keyGenAlgorithm = 'PBKDF2' ; keyGenIterations = 250000 ; keyGenHashFunction = 'SHA-256' ; encryptionAlgorithm = 'AES-GCM' ; static $inject = [ '$q' , 'LogService' , 'StoreService' , 'UtilityService' ] ; constructor ( $q , LogSvc , StoreSvc , UtilitySvc ) { this . $q = $q ; this . logSvc = LogSvc ; this . storeSvc = StoreSvc ; this . utilitySvc = UtilitySvc ; } concatUint8Arrays ( firstArr = new Uint8Array ( ) , secondArr = new Uint8Array ( ) ) { const totalLength = firstArr . length + secondArr . length ; const result = new Uint8Array ( totalLength ) ; result . set ( firstArr , 0 ) ; result . set ( secondArr , firstArr . length ) ; return result ; } decryptData ( encryptedData ) { if ( ! encryptedData ) { return this . $q . resolve ( '' ) ; } return this . storeSvc . get ( [ StoreKey . Password , StoreKey . SyncId ] ) . then ( ( storeContent ) => { const { password , syncId } = storeContent ; if ( ! password || ! syncId ) { throw new ClientDataNotFoundError ( ) ; } const keyData = base64js . toByteArray ( password ) ; const encryptedBytes = base64js . toByteArray ( encryptedData ) ; const iv = encryptedBytes . slice ( 0 , 16 ) ; const encryptedDataBytes = encryptedBytes . slice ( 16 ) . buffer ; return crypto . subtle . importKey ( 'raw' , keyData , { length : keyData . length , name : this . encryptionAlgorithm } , false , [ 'decrypt' ] ) . then ( ( key ) => { return crypto . subtle . decrypt ( { name : this . encryptionAlgorithm , iv } , key , encryptedDataBytes ) ; } ) . then ( ( decryptedBytes ) => { if ( ! decryptedBytes ) { throw new Error ( 'Unable to decrypt data.' ) ; } const decryptedData = lzutf8 . decompress ( new Uint8Array ( decryptedBytes ) ) as string ; return decryptedData ; } ) ; } ) . catch ( ( err ) => { this . logSvc . logWarning ( 'Decryption failed' ) ; throw new InvalidCredentialsError ( undefined , err ) ; } ) ; } encryptData ( data ) { if ( ! data ) { return this . $q . resolve ( '' ) ; } if ( ! angular . isString ( data ) ) { throw new ArgumentError ( 'Argument must be a string' ) ; } return this . storeSvc . get < string > ( StoreKey . Password ) . then ( ( password ) => { if ( angular . isUndefined ( password ) ) { throw new ClientDataNotFoundError ( ) ; } const keyData = base64js . toByteArray ( password ) ; const iv = crypto . getRandomValues ( new Uint8Array ( 16 ) ) ; return crypto . subtle . importKey ( 'raw' , keyData , { length : keyData . length , name : this . encryptionAlgorithm } , false , [ 'encrypt' ] ) . then ( ( key ) => { const compressedData = lzutf8 . compress ( data ) ; return crypto . subtle . encrypt ( { name : this . encryptionAlgorithm , iv } , key , compressedData ) ; } ) . then ( ( encryptedData ) => { const combinedData = this . concatUint8Arrays ( iv , new Uint8Array ( encryptedData ) ) ; return base64js . fromByteArray ( combinedData ) ; } ) ; } ) . catch ( ( err ) => { this . logSvc . logWarning ( 'Encryption failed' ) ; throw new InvalidCredentialsError ( undefined , err ) ; } ) ; } getPasswordHash ( password , salt ) { const encoder = new TextEncoder ( ) ; const encodedSalt = encoder . encode ( salt ) ; return this . utilitySvc . getSyncVersion ( ) . then ( ( syncVersion ) => { if ( ! syncVersion ) { return this . $q . resolve ( password ) ; } const keyData = encoder . encode ( password ) ; return crypto . subtle . importKey ( 'raw' , keyData , { length : keyData . length , name : this . keyGenAlgorithm } , false , [ 'deriveKey' ] ) . then ( ( importedKey ) => { return crypto . subtle . deriveKey ( { name : this . keyGenAlgorithm , salt : encodedSalt , iterations : this . keyGenIterations , hash : this . keyGenHashFunction } , importedKey , { name : this . encryptionAlgorithm , length : 256 } , true , [ 'encrypt' , 'decrypt' ] ) ; } ) . then ( ( derivedKey ) => { return crypto . subtle . exportKey ( 'raw' , derivedKey ) ; } ) . then ( ( exportedKey ) => { const base64Key = base64js . fromByteArray ( new Uint8Array ( exportedKey ) ) ; return base64Key ; } ) ; } ) ; } }