2018-03-09 22:59:12 +02:00
const BaseItem = require ( 'lib/models/BaseItem' ) ;
2019-05-28 23:05:11 +02:00
const MasterKey = require ( 'lib/models/MasterKey' ) ;
2018-10-08 20:11:53 +02:00
const Resource = require ( 'lib/models/Resource' ) ;
2019-05-12 16:53:42 +02:00
const ResourceService = require ( 'lib/services/ResourceService' ) ;
2018-03-09 22:59:12 +02:00
const { Logger } = require ( 'lib/logger.js' ) ;
2019-05-22 16:56:07 +02:00
const EventEmitter = require ( 'events' ) ;
2017-12-14 20:53:08 +02:00
2017-12-14 19:58:10 +02:00
class DecryptionWorker {
constructor ( ) {
2018-03-09 22:59:12 +02:00
this . state _ = 'idle' ;
2017-12-31 16:23:05 +02:00
this . logger _ = new Logger ( ) ;
2017-12-14 20:53:08 +02:00
2019-09-13 00:16:42 +02:00
this . dispatch = ( ) => { } ;
2017-12-14 21:39:13 +02:00
this . scheduleId _ = null ;
2019-05-22 16:56:07 +02:00
this . eventEmitter _ = new EventEmitter ( ) ;
2019-06-08 00:11:08 +02:00
this . kvStore _ = null ;
this . maxDecryptionAttempts _ = 2 ;
2020-02-22 13:25:16 +02:00
this . startCalls _ = [ ] ;
2017-12-14 21:39:13 +02:00
}
setLogger ( l ) {
this . logger _ = l ;
}
logger ( ) {
return this . logger _ ;
2017-12-14 20:53:08 +02:00
}
2019-05-22 16:56:07 +02:00
on ( eventName , callback ) {
return this . eventEmitter _ . on ( eventName , callback ) ;
}
off ( eventName , callback ) {
return this . eventEmitter _ . removeListener ( eventName , callback ) ;
}
2017-12-14 20:53:08 +02:00
static instance ( ) {
2020-02-27 20:25:42 +02:00
if ( DecryptionWorker . instance _ ) return DecryptionWorker . instance _ ;
DecryptionWorker . instance _ = new DecryptionWorker ( ) ;
return DecryptionWorker . instance _ ;
2017-12-14 20:53:08 +02:00
}
2017-12-14 21:39:13 +02:00
setEncryptionService ( v ) {
this . encryptionService _ = v ;
}
2019-06-08 00:11:08 +02:00
setKvStore ( v ) {
this . kvStore _ = v ;
}
2017-12-14 21:39:13 +02:00
encryptionService ( ) {
2018-03-09 22:59:12 +02:00
if ( ! this . encryptionService _ ) throw new Error ( 'DecryptionWorker.encryptionService_ is not set!!' ) ;
2017-12-14 20:53:08 +02:00
return this . encryptionService _ ;
2017-12-14 19:58:10 +02:00
}
2019-06-08 00:11:08 +02:00
kvStore ( ) {
if ( ! this . kvStore _ ) throw new Error ( 'DecryptionWorker.kvStore_ is not set!!' ) ;
return this . kvStore _ ;
}
2017-12-14 21:39:13 +02:00
async scheduleStart ( ) {
if ( this . scheduleId _ ) return ;
this . scheduleId _ = setTimeout ( ( ) => {
this . scheduleId _ = null ;
2017-12-26 12:38:53 +02:00
this . start ( {
2018-10-08 20:11:53 +02:00
masterKeyNotLoadedHandler : 'dispatch' ,
2017-12-26 12:38:53 +02:00
} ) ;
2017-12-14 21:39:13 +02:00
} , 1000 ) ;
}
2019-06-08 00:11:08 +02:00
async decryptionDisabledItems ( ) {
let items = await this . kvStore ( ) . searchByPrefix ( 'decrypt:' ) ;
items = items . filter ( item => item . value > this . maxDecryptionAttempts _ ) ;
items = items . map ( item => {
const s = item . key . split ( ':' ) ;
return {
type _ : Number ( s [ 1 ] ) ,
id : s [ 2 ] ,
} ;
} ) ;
return items ;
}
async clearDisabledItem ( typeId , itemId ) {
2019-09-19 23:51:18 +02:00
await this . kvStore ( ) . deleteValue ( ` decrypt: ${ typeId } : ${ itemId } ` ) ;
2019-06-08 00:11:08 +02:00
}
2018-06-10 18:43:24 +02:00
dispatchReport ( report ) {
const action = Object . assign ( { } , report ) ;
action . type = 'DECRYPTION_WORKER_SET' ;
this . dispatch ( action ) ;
}
2020-02-22 13:25:16 +02:00
async start _ ( options = null ) {
2017-12-26 12:38:53 +02:00
if ( options === null ) options = { } ;
2018-10-08 20:11:53 +02:00
if ( ! ( 'masterKeyNotLoadedHandler' in options ) ) options . masterKeyNotLoadedHandler = 'throw' ;
2019-06-08 00:11:08 +02:00
if ( ! ( 'errorHandler' in options ) ) options . errorHandler = 'log' ;
2017-12-26 12:38:53 +02:00
2018-03-09 22:59:12 +02:00
if ( this . state _ !== 'idle' ) {
2019-09-19 23:51:18 +02:00
this . logger ( ) . debug ( ` DecryptionWorker: cannot start because state is " ${ this . state _ } " ` ) ;
2017-12-26 12:38:53 +02:00
return ;
}
2019-06-10 09:55:36 +02:00
// Note: the logic below is an optimisation to avoid going through the loop if no master key exists
// or if none is loaded. It means this logic needs to be duplicate a bit what's in the loop, like the
// "throw" and "dispatch" logic.
2019-05-28 23:05:11 +02:00
const loadedMasterKeyCount = await this . encryptionService ( ) . loadedMasterKeysCount ( ) ;
if ( ! loadedMasterKeyCount ) {
this . logger ( ) . info ( 'DecryptionWorker: cannot start because no master key is currently loaded.' ) ;
const ids = await MasterKey . allIds ( ) ;
if ( ids . length ) {
2019-06-10 09:55:36 +02:00
if ( options . masterKeyNotLoadedHandler === 'throw' ) {
// By trying to load the master key here, we throw the "masterKeyNotLoaded" error
// which the caller needs.
await this . encryptionService ( ) . loadedMasterKey ( ids [ 0 ] ) ;
} else {
this . dispatch ( {
type : 'MASTERKEY_SET_NOT_LOADED' ,
ids : ids ,
} ) ;
}
2019-05-28 23:05:11 +02:00
}
return ;
}
2018-03-09 22:59:12 +02:00
this . logger ( ) . info ( 'DecryptionWorker: starting decryption...' ) ;
2017-12-14 19:58:10 +02:00
2018-03-09 22:59:12 +02:00
this . state _ = 'started' ;
2017-12-14 20:53:08 +02:00
let excludedIds = [ ] ;
2020-03-13 19:42:50 +02:00
this . dispatch ( { type : 'ENCRYPTION_HAS_DISABLED_ITEMS' , value : false } ) ;
2018-06-10 18:43:24 +02:00
this . dispatchReport ( { state : 'started' } ) ;
2017-12-14 21:39:13 +02:00
try {
2018-01-28 19:37:29 +02:00
const notLoadedMasterKeyDisptaches = [ ] ;
2017-12-14 21:39:13 +02:00
while ( true ) {
const result = await BaseItem . itemsThatNeedDecryption ( excludedIds ) ;
const items = result . items ;
for ( let i = 0 ; i < items . length ; i ++ ) {
const item = items [ i ] ;
2018-01-31 21:51:29 +02:00
2017-12-14 21:39:13 +02:00
const ItemClass = BaseItem . itemClass ( item ) ;
2018-06-10 18:43:24 +02:00
this . dispatchReport ( {
itemIndex : i ,
itemCount : items . length ,
} ) ;
2019-06-08 00:11:08 +02:00
2019-09-19 23:51:18 +02:00
const counterKey = ` decrypt: ${ item . type _ } : ${ item . id } ` ;
2019-06-08 00:11:08 +02:00
const clearDecryptionCounter = async ( ) => {
await this . kvStore ( ) . deleteValue ( counterKey ) ;
2019-07-29 15:43:53 +02:00
} ;
2018-03-05 20:21:42 +02:00
// Don't log in production as it results in many messages when importing many items
2019-05-28 23:05:11 +02:00
// this.logger().debug('DecryptionWorker: decrypting: ' + item.id + ' (' + ItemClass.tableName() + ')');
2017-12-14 21:39:13 +02:00
try {
2019-06-08 00:11:08 +02:00
const decryptCounter = await this . kvStore ( ) . incValue ( counterKey ) ;
if ( decryptCounter > this . maxDecryptionAttempts _ ) {
2020-03-13 19:42:50 +02:00
this . logger ( ) . debug ( ` DecryptionWorker: ${ item . id } decryption has failed more than 2 times - skipping it ` ) ;
this . dispatch ( { type : 'ENCRYPTION_HAS_DISABLED_ITEMS' , value : true } ) ;
2019-06-08 00:11:08 +02:00
excludedIds . push ( item . id ) ;
continue ;
}
2019-05-22 16:56:07 +02:00
const decryptedItem = await ItemClass . decrypt ( item ) ;
2019-06-08 00:11:08 +02:00
await clearDecryptionCounter ( ) ;
2019-05-22 16:56:07 +02:00
if ( decryptedItem . type _ === Resource . modelType ( ) && ! ! decryptedItem . encryption _blob _encrypted ) {
// itemsThatNeedDecryption() will return the resource again if the blob has not been decrypted,
// but that will result in an infinite loop if the blob simply has not been downloaded yet.
2019-07-29 15:43:53 +02:00
// So skip the ID for now, and the service will try to decrypt the blob again the next time.
2019-05-22 16:56:07 +02:00
excludedIds . push ( decryptedItem . id ) ;
}
if ( decryptedItem . type _ === Resource . modelType ( ) && ! decryptedItem . encryption _blob _encrypted ) {
this . eventEmitter _ . emit ( 'resourceDecrypted' , { id : decryptedItem . id } ) ;
}
2019-05-28 23:05:11 +02:00
if ( decryptedItem . type _ === Resource . modelType ( ) && ! decryptedItem . encryption _applied && ! ! decryptedItem . encryption _blob _encrypted ) {
this . eventEmitter _ . emit ( 'resourceMetadataButNotBlobDecrypted' , { id : decryptedItem . id } ) ;
}
2017-12-14 21:39:13 +02:00
} catch ( error ) {
2018-02-14 17:28:56 +02:00
excludedIds . push ( item . id ) ;
2019-05-22 16:56:07 +02:00
2018-10-08 20:11:53 +02:00
if ( error . code === 'masterKeyNotLoaded' && options . masterKeyNotLoadedHandler === 'dispatch' ) {
2018-01-28 19:37:29 +02:00
if ( notLoadedMasterKeyDisptaches . indexOf ( error . masterKeyId ) < 0 ) {
this . dispatch ( {
2018-03-09 22:59:12 +02:00
type : 'MASTERKEY_ADD_NOT_LOADED' ,
2018-01-28 19:37:29 +02:00
id : error . masterKeyId ,
} ) ;
notLoadedMasterKeyDisptaches . push ( error . masterKeyId ) ;
}
2019-06-08 00:11:08 +02:00
await clearDecryptionCounter ( ) ;
2017-12-14 21:39:13 +02:00
continue ;
}
2018-02-26 21:25:54 +02:00
2018-10-08 20:11:53 +02:00
if ( error . code === 'masterKeyNotLoaded' && options . masterKeyNotLoadedHandler === 'throw' ) {
2019-06-08 00:11:08 +02:00
await clearDecryptionCounter ( ) ;
2018-02-26 21:25:54 +02:00
throw error ;
}
2019-06-08 00:11:08 +02:00
if ( options . errorHandler === 'log' ) {
2019-09-19 23:51:18 +02:00
this . logger ( ) . warn ( ` DecryptionWorker: error for: ${ item . id } ( ${ ItemClass . tableName ( ) } ) ` , error , item ) ;
2019-06-08 00:11:08 +02:00
} else {
throw error ;
}
2017-12-14 20:53:08 +02:00
}
}
2017-12-14 21:39:13 +02:00
if ( ! result . hasMore ) break ;
}
} catch ( error ) {
2018-03-09 22:59:12 +02:00
this . logger ( ) . error ( 'DecryptionWorker:' , error ) ;
this . state _ = 'idle' ;
2018-06-10 18:43:24 +02:00
this . dispatchReport ( { state : 'idle' } ) ;
2017-12-26 12:38:53 +02:00
throw error ;
2017-12-14 20:53:08 +02:00
}
2017-12-14 21:39:13 +02:00
2019-05-12 16:53:42 +02:00
// 2019-05-12: Temporary to set the file size of the resources
// that weren't set in migration/20.js due to being on the sync target
await ResourceService . autoSetFileSizes ( ) ;
2018-03-09 22:59:12 +02:00
this . logger ( ) . info ( 'DecryptionWorker: completed decryption.' ) ;
2017-12-26 12:38:53 +02:00
2019-05-22 16:56:07 +02:00
const downloadedButEncryptedBlobCount = await Resource . downloadedButEncryptedBlobCount ( ) ;
2018-03-09 22:59:12 +02:00
this . state _ = 'idle' ;
2019-05-22 16:56:07 +02:00
2019-05-28 23:05:11 +02:00
this . dispatchReport ( { state : 'idle' } ) ;
2019-05-22 16:56:07 +02:00
if ( downloadedButEncryptedBlobCount ) {
2019-09-19 23:51:18 +02:00
this . logger ( ) . info ( ` DecryptionWorker: Some resources have been downloaded but are not decrypted yet. Scheduling another decryption. Resource count: ${ downloadedButEncryptedBlobCount } ` ) ;
2019-05-22 16:56:07 +02:00
this . scheduleStart ( ) ;
}
2017-12-14 19:58:10 +02:00
}
2020-02-22 13:25:16 +02:00
async start ( options ) {
this . startCalls _ . push ( true ) ;
try {
await this . start _ ( options ) ;
} finally {
this . startCalls _ . pop ( ) ;
}
}
2020-02-27 20:25:42 +02:00
async destroy ( ) {
this . eventEmitter _ . removeAllListeners ( ) ;
if ( this . scheduleId _ ) {
clearTimeout ( this . scheduleId _ ) ;
this . scheduleId _ = null ;
}
this . eventEmitter _ = null ;
DecryptionWorker . instance _ = null ;
2020-02-22 13:25:16 +02:00
return new Promise ( ( resolve ) => {
const iid = setInterval ( ( ) => {
if ( ! this . startCalls _ . length ) {
clearInterval ( iid ) ;
resolve ( ) ;
}
} , 100 ) ;
} ) ;
}
2017-12-14 19:58:10 +02:00
}
2020-02-27 20:25:42 +02:00
DecryptionWorker . instance _ = null ;
2019-07-29 15:43:53 +02:00
module . exports = DecryptionWorker ;