2021-01-22 17:41:11 +00:00
import BaseItem , { ItemsThatNeedDecryptionResult } from '../models/BaseItem' ;
import BaseModel from '../BaseModel' ;
import MasterKey from '../models/MasterKey' ;
import Resource from '../models/Resource' ;
import ResourceService from './ResourceService' ;
2023-07-27 16:05:56 +01:00
import Logger from '@joplin/utils/Logger' ;
2021-01-22 17:41:11 +00:00
import shim from '../shim' ;
import KvStore from './KvStore' ;
2021-08-30 14:15:35 +01:00
import EncryptionService from './e2ee/EncryptionService' ;
2021-01-22 17:41:11 +00:00
2019-05-22 15:56:07 +01:00
const EventEmitter = require ( 'events' ) ;
2017-12-14 18:53:08 +00:00
2021-08-07 12:22:37 +01:00
interface DecryptionResult {
skippedItemCount? : number ;
decryptedItemCounts? : number ;
decryptedItemCount? : number ;
error : any ;
}
2021-01-22 17:41:11 +00:00
export default class DecryptionWorker {
public static instance_ : DecryptionWorker = null ;
2023-06-30 09:07:03 +01:00
private state_ = 'idle' ;
2021-01-22 17:41:11 +00:00
private logger_ : Logger ;
2023-06-30 10:30:29 +01:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2021-01-22 17:41:11 +00:00
public dispatch : Function = ( ) = > { } ;
private scheduleId_ : any = null ;
private eventEmitter_ : any ;
private kvStore_ : KvStore = null ;
private maxDecryptionAttempts_ = 2 ;
private startCalls_ : boolean [ ] = [ ] ;
2021-08-30 14:15:35 +01:00
private encryptionService_ : EncryptionService = null ;
2021-01-22 17:41:11 +00:00
2023-03-06 14:22:01 +00:00
public constructor ( ) {
2018-03-09 20:59:12 +00:00
this . state_ = 'idle' ;
2017-12-31 15:23:05 +01:00
this . logger_ = new Logger ( ) ;
2019-05-22 15:56:07 +01:00
this . eventEmitter_ = new EventEmitter ( ) ;
2017-12-14 19:39:13 +00:00
}
2023-03-06 14:22:01 +00:00
public setLogger ( l : Logger ) {
2017-12-14 19:39:13 +00:00
this . logger_ = l ;
}
2023-03-06 14:22:01 +00:00
public logger() {
2017-12-14 19:39:13 +00:00
return this . logger_ ;
2017-12-14 18:53:08 +00:00
}
2023-06-30 10:30:29 +01:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2023-03-06 14:22:01 +00:00
public on ( eventName : string , callback : Function ) {
2019-05-22 15:56:07 +01:00
return this . eventEmitter_ . on ( eventName , callback ) ;
}
2023-06-30 10:30:29 +01:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2023-03-06 14:22:01 +00:00
public off ( eventName : string , callback : Function ) {
2019-05-22 15:56:07 +01:00
return this . eventEmitter_ . removeListener ( eventName , callback ) ;
}
2023-03-06 14:22:01 +00:00
public static instance() {
2020-02-28 05:25:42 +11:00
if ( DecryptionWorker . instance_ ) return DecryptionWorker . instance_ ;
DecryptionWorker . instance_ = new DecryptionWorker ( ) ;
return DecryptionWorker . instance_ ;
2017-12-14 18:53:08 +00:00
}
2023-03-06 14:22:01 +00:00
public setEncryptionService ( v : any ) {
2017-12-14 19:39:13 +00:00
this . encryptionService_ = v ;
}
2023-03-06 14:22:01 +00:00
public setKvStore ( v : KvStore ) {
2019-06-07 23:11:08 +01:00
this . kvStore_ = v ;
}
2023-03-06 14:22:01 +00:00
public encryptionService() {
2018-03-09 20:59:12 +00:00
if ( ! this . encryptionService_ ) throw new Error ( 'DecryptionWorker.encryptionService_ is not set!!' ) ;
2017-12-14 18:53:08 +00:00
return this . encryptionService_ ;
2017-12-14 17:58:10 +00:00
}
2023-03-06 14:22:01 +00:00
public kvStore() {
2019-06-07 23:11:08 +01:00
if ( ! this . kvStore_ ) throw new Error ( 'DecryptionWorker.kvStore_ is not set!!' ) ;
return this . kvStore_ ;
}
2023-03-06 14:22:01 +00:00
public async scheduleStart() {
2017-12-14 19:39:13 +00:00
if ( this . scheduleId_ ) return ;
2020-10-09 18:35:46 +01:00
this . scheduleId_ = shim . setTimeout ( ( ) = > {
2017-12-14 19:39:13 +00:00
this . scheduleId_ = null ;
2021-01-22 17:41:11 +00:00
void this . start ( {
2018-10-08 19:11:53 +01:00
masterKeyNotLoadedHandler : 'dispatch' ,
2017-12-26 11:38:53 +01:00
} ) ;
2017-12-14 19:39:13 +00:00
} , 1000 ) ;
}
2023-03-06 14:22:01 +00:00
public async decryptionDisabledItems() {
2019-06-07 23:11:08 +01:00
let items = await this . kvStore ( ) . searchByPrefix ( 'decrypt:' ) ;
2020-05-21 09:14:33 +01:00
items = items . filter ( item = > item . value > this . maxDecryptionAttempts_ ) ;
items = items . map ( item = > {
2019-06-07 23:11:08 +01:00
const s = item . key . split ( ':' ) ;
return {
type_ : Number ( s [ 1 ] ) ,
id : s [ 2 ] ,
} ;
} ) ;
return items ;
}
2023-03-06 14:22:01 +00:00
public async clearDisabledItem ( typeId : string , itemId : string ) {
2019-09-19 22:51:18 +01:00
await this . kvStore ( ) . deleteValue ( ` decrypt: ${ typeId } : ${ itemId } ` ) ;
2019-06-07 23:11:08 +01:00
}
2023-03-06 14:22:01 +00:00
public async clearDisabledItems() {
2020-04-08 18:02:31 +01:00
await this . kvStore ( ) . deleteByPrefix ( 'decrypt:' ) ;
}
2023-03-06 14:22:01 +00:00
public dispatchReport ( report : any ) {
2023-06-01 12:02:36 +01:00
const action = { . . . report } ;
2018-06-10 17:43:24 +01:00
action . type = 'DECRYPTION_WORKER_SET' ;
this . dispatch ( action ) ;
}
2021-08-07 12:22:37 +01:00
private async start_ ( options : any = null ) : Promise < DecryptionResult > {
2017-12-26 11:38:53 +01:00
if ( options === null ) options = { } ;
2018-10-08 19:11:53 +01:00
if ( ! ( 'masterKeyNotLoadedHandler' in options ) ) options . masterKeyNotLoadedHandler = 'throw' ;
2019-06-07 23:11:08 +01:00
if ( ! ( 'errorHandler' in options ) ) options . errorHandler = 'log' ;
2017-12-26 11:38:53 +01:00
2018-03-09 20:59:12 +00:00
if ( this . state_ !== 'idle' ) {
2020-10-29 23:37:19 +00:00
const msg = ` DecryptionWorker: cannot start because state is " ${ this . state_ } " ` ;
this . logger ( ) . debug ( msg ) ;
return { error : new Error ( msg ) } ;
2017-12-26 11:38:53 +01:00
}
2019-06-10 08:55:36 +01: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 22:05:11 +01:00
const loadedMasterKeyCount = await this . encryptionService ( ) . loadedMasterKeysCount ( ) ;
if ( ! loadedMasterKeyCount ) {
2020-10-29 23:37:19 +00:00
const msg = 'DecryptionWorker: cannot start because no master key is currently loaded.' ;
this . logger ( ) . info ( msg ) ;
2019-05-28 22:05:11 +01:00
const ids = await MasterKey . allIds ( ) ;
2021-08-30 14:15:35 +01:00
// Note that the current implementation means that a warning will be
// displayed even if the user has no encrypted note. Just having
// encrypted master key is sufficient. Not great but good enough for
// now.
2019-05-28 22:05:11 +01:00
if ( ids . length ) {
2019-06-10 08:55:36 +01: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 22:05:11 +01:00
}
2020-10-29 23:37:19 +00:00
return { error : new Error ( msg ) } ;
2019-05-28 22:05:11 +01:00
}
2018-03-09 20:59:12 +00:00
this . logger ( ) . info ( 'DecryptionWorker: starting decryption...' ) ;
2017-12-14 17:58:10 +00:00
2018-03-09 20:59:12 +00:00
this . state_ = 'started' ;
2017-12-14 18:53:08 +00:00
2020-03-13 23:46:14 +00:00
const excludedIds = [ ] ;
2021-01-22 17:41:11 +00:00
const decryptedItemCounts : any = { } ;
2020-05-13 16:28:54 +01:00
let skippedItemCount = 0 ;
2017-12-14 18:53:08 +00:00
2020-03-13 17:42:50 +00:00
this . dispatch ( { type : 'ENCRYPTION_HAS_DISABLED_ITEMS' , value : false } ) ;
2018-06-10 17:43:24 +01:00
this . dispatchReport ( { state : 'started' } ) ;
2017-12-14 19:39:13 +00:00
try {
2018-01-28 17:37:29 +00:00
const notLoadedMasterKeyDisptaches = [ ] ;
2017-12-14 19:39:13 +00:00
while ( true ) {
2021-01-22 17:41:11 +00:00
const result : ItemsThatNeedDecryptionResult = await BaseItem . itemsThatNeedDecryption ( excludedIds ) ;
2017-12-14 19:39:13 +00:00
const items = result . items ;
for ( let i = 0 ; i < items . length ; i ++ ) {
const item = items [ i ] ;
2018-01-31 19:51:29 +00:00
2017-12-14 19:39:13 +00:00
const ItemClass = BaseItem . itemClass ( item ) ;
2018-06-10 17:43:24 +01:00
this . dispatchReport ( {
itemIndex : i ,
itemCount : items.length ,
} ) ;
2019-06-07 23:11:08 +01:00
2019-09-19 22:51:18 +01:00
const counterKey = ` decrypt: ${ item . type_ } : ${ item . id } ` ;
2019-06-07 23:11:08 +01:00
const clearDecryptionCounter = async ( ) = > {
await this . kvStore ( ) . deleteValue ( counterKey ) ;
2019-07-29 15:43:53 +02:00
} ;
2018-03-05 18:21:42 +00:00
// Don't log in production as it results in many messages when importing many items
2019-05-28 22:05:11 +01:00
// this.logger().debug('DecryptionWorker: decrypting: ' + item.id + ' (' + ItemClass.tableName() + ')');
2017-12-14 19:39:13 +00:00
try {
2019-06-07 23:11:08 +01:00
const decryptCounter = await this . kvStore ( ) . incValue ( counterKey ) ;
if ( decryptCounter > this . maxDecryptionAttempts_ ) {
2020-04-08 01:00:01 +01:00
this . logger ( ) . debug ( ` DecryptionWorker: ${ BaseModel . modelTypeToName ( item . type_ ) } ${ item . id } : Decryption has failed more than 2 times - skipping it ` ) ;
2020-03-13 17:42:50 +00:00
this . dispatch ( { type : 'ENCRYPTION_HAS_DISABLED_ITEMS' , value : true } ) ;
2020-05-13 16:28:54 +01:00
skippedItemCount ++ ;
2019-06-07 23:11:08 +01:00
excludedIds . push ( item . id ) ;
continue ;
}
2019-05-22 15:56:07 +01:00
const decryptedItem = await ItemClass . decrypt ( item ) ;
2019-06-07 23:11:08 +01:00
await clearDecryptionCounter ( ) ;
2020-04-19 10:11:46 +01:00
if ( ! decryptedItemCounts [ decryptedItem . type_ ] ) decryptedItemCounts [ decryptedItem . type_ ] = 0 ;
decryptedItemCounts [ decryptedItem . type_ ] ++ ;
2019-05-22 15:56:07 +01: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 15:56:07 +01:00
excludedIds . push ( decryptedItem . id ) ;
}
if ( decryptedItem . type_ === Resource . modelType ( ) && ! decryptedItem . encryption_blob_encrypted ) {
this . eventEmitter_ . emit ( 'resourceDecrypted' , { id : decryptedItem.id } ) ;
}
2019-05-28 22:05:11 +01:00
if ( decryptedItem . type_ === Resource . modelType ( ) && ! decryptedItem . encryption_applied && ! ! decryptedItem . encryption_blob_encrypted ) {
this . eventEmitter_ . emit ( 'resourceMetadataButNotBlobDecrypted' , { id : decryptedItem.id } ) ;
}
2017-12-14 19:39:13 +00:00
} catch ( error ) {
2018-02-14 15:28:56 +00:00
excludedIds . push ( item . id ) ;
2019-05-22 15:56:07 +01:00
2018-10-08 19:11:53 +01:00
if ( error . code === 'masterKeyNotLoaded' && options . masterKeyNotLoadedHandler === 'dispatch' ) {
2018-01-28 17:37:29 +00:00
if ( notLoadedMasterKeyDisptaches . indexOf ( error . masterKeyId ) < 0 ) {
this . dispatch ( {
2018-03-09 20:59:12 +00:00
type : 'MASTERKEY_ADD_NOT_LOADED' ,
2018-01-28 17:37:29 +00:00
id : error.masterKeyId ,
} ) ;
notLoadedMasterKeyDisptaches . push ( error . masterKeyId ) ;
}
2019-06-07 23:11:08 +01:00
await clearDecryptionCounter ( ) ;
2017-12-14 19:39:13 +00:00
continue ;
}
2018-02-26 19:25:54 +00:00
2018-10-08 19:11:53 +01:00
if ( error . code === 'masterKeyNotLoaded' && options . masterKeyNotLoadedHandler === 'throw' ) {
2019-06-07 23:11:08 +01:00
await clearDecryptionCounter ( ) ;
2018-02-26 19:25:54 +00:00
throw error ;
}
2019-06-07 23:11:08 +01:00
if ( options . errorHandler === 'log' ) {
2019-09-19 22:51:18 +01:00
this . logger ( ) . warn ( ` DecryptionWorker: error for: ${ item . id } ( ${ ItemClass . tableName ( ) } ) ` , error , item ) ;
2019-06-07 23:11:08 +01:00
} else {
throw error ;
}
2017-12-14 18:53:08 +00:00
}
}
2017-12-14 19:39:13 +00:00
if ( ! result . hasMore ) break ;
}
} catch ( error ) {
2018-03-09 20:59:12 +00:00
this . logger ( ) . error ( 'DecryptionWorker:' , error ) ;
this . state_ = 'idle' ;
2018-06-10 17:43:24 +01:00
this . dispatchReport ( { state : 'idle' } ) ;
2017-12-26 11:38:53 +01:00
throw error ;
2017-12-14 18:53:08 +00:00
}
2017-12-14 19:39:13 +00:00
2019-05-12 15:53:42 +01: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 20:59:12 +00:00
this . logger ( ) . info ( 'DecryptionWorker: completed decryption.' ) ;
2017-12-26 11:38:53 +01:00
2020-04-08 01:00:01 +01:00
const downloadedButEncryptedBlobCount = await Resource . downloadedButEncryptedBlobCount ( excludedIds ) ;
2019-05-22 15:56:07 +01:00
2018-03-09 20:59:12 +00:00
this . state_ = 'idle' ;
2019-05-22 15:56:07 +01:00
2020-05-13 16:28:54 +01:00
let decryptedItemCount = 0 ;
for ( const itemType in decryptedItemCounts ) decryptedItemCount += decryptedItemCounts [ itemType ] ;
2021-08-07 12:22:37 +01:00
const finalReport : DecryptionResult = {
2020-05-13 16:28:54 +01:00
skippedItemCount : skippedItemCount ,
2020-04-19 10:11:46 +01:00
decryptedItemCounts : decryptedItemCounts ,
2020-05-13 16:28:54 +01:00
decryptedItemCount : decryptedItemCount ,
2021-08-07 12:22:37 +01:00
error : null ,
2020-05-13 16:28:54 +01:00
} ;
2023-06-01 12:02:36 +01:00
this . dispatchReport ( { . . . finalReport , state : 'idle' } ) ;
2019-05-28 22:05:11 +01:00
2019-05-22 15:56:07 +01:00
if ( downloadedButEncryptedBlobCount ) {
2019-09-19 22:51:18 +01:00
this . logger ( ) . info ( ` DecryptionWorker: Some resources have been downloaded but are not decrypted yet. Scheduling another decryption. Resource count: ${ downloadedButEncryptedBlobCount } ` ) ;
2021-01-22 17:41:11 +00:00
void this . scheduleStart ( ) ;
2019-05-22 15:56:07 +01:00
}
2020-05-13 16:28:54 +01:00
return finalReport ;
2017-12-14 17:58:10 +00:00
}
2020-02-22 22:25:16 +11:00
2021-08-07 12:22:37 +01:00
public async start ( options : any = { } ) {
2020-02-22 22:25:16 +11:00
this . startCalls_ . push ( true ) ;
2020-05-13 16:28:54 +01:00
let output = null ;
2020-02-22 22:25:16 +11:00
try {
2020-05-13 16:28:54 +01:00
output = await this . start_ ( options ) ;
2020-02-22 22:25:16 +11:00
} finally {
this . startCalls_ . pop ( ) ;
}
2020-05-13 16:28:54 +01:00
return output ;
2020-02-22 22:25:16 +11:00
}
2023-03-06 14:22:01 +00:00
public async destroy() {
2020-02-28 05:25:42 +11:00
this . eventEmitter_ . removeAllListeners ( ) ;
if ( this . scheduleId_ ) {
2020-10-09 18:35:46 +01:00
shim . clearTimeout ( this . scheduleId_ ) ;
2020-02-28 05:25:42 +11:00
this . scheduleId_ = null ;
}
this . eventEmitter_ = null ;
DecryptionWorker . instance_ = null ;
2020-02-22 22:25:16 +11:00
return new Promise ( ( resolve ) = > {
2020-10-09 18:35:46 +01:00
const iid = shim . setInterval ( ( ) = > {
2020-02-22 22:25:16 +11:00
if ( ! this . startCalls_ . length ) {
2020-10-09 18:35:46 +01:00
shim . clearInterval ( iid ) ;
2021-01-22 17:41:11 +00:00
resolve ( null ) ;
2020-02-22 22:25:16 +11:00
}
} , 100 ) ;
} ) ;
}
2017-12-14 17:58:10 +00:00
}