2021-01-22 19:41:11 +02: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 17:05:56 +02:00
import Logger from '@joplin/utils/Logger' ;
2021-01-22 19:41:11 +02:00
import shim from '../shim' ;
import KvStore from './KvStore' ;
2021-08-30 15:15:35 +02:00
import EncryptionService from './e2ee/EncryptionService' ;
2021-01-22 19:41:11 +02:00
2019-05-22 16:56:07 +02:00
const EventEmitter = require ( 'events' ) ;
2017-12-14 20:53:08 +02:00
2021-08-07 13:22:37 +02:00
interface DecryptionResult {
skippedItemCount? : number ;
decryptedItemCounts? : number ;
decryptedItemCount? : number ;
error : any ;
}
2021-01-22 19:41:11 +02:00
export default class DecryptionWorker {
public static instance_ : DecryptionWorker = null ;
2023-06-30 10:07:03 +02:00
private state_ = 'idle' ;
2021-01-22 19:41:11 +02:00
private logger_ : Logger ;
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2021-01-22 19:41:11 +02:00
public dispatch : Function = ( ) = > { } ;
private scheduleId_ : any = null ;
private eventEmitter_ : any ;
private kvStore_ : KvStore = null ;
private maxDecryptionAttempts_ = 2 ;
private startCalls_ : boolean [ ] = [ ] ;
2021-08-30 15:15:35 +02:00
private encryptionService_ : EncryptionService = null ;
2021-01-22 19:41:11 +02:00
2023-03-06 16:22:01 +02:00
public constructor ( ) {
2018-03-09 22:59:12 +02:00
this . state_ = 'idle' ;
2017-12-31 16:23:05 +02:00
this . logger_ = new Logger ( ) ;
2019-05-22 16:56:07 +02:00
this . eventEmitter_ = new EventEmitter ( ) ;
2017-12-14 21:39:13 +02:00
}
2023-03-06 16:22:01 +02:00
public setLogger ( l : Logger ) {
2017-12-14 21:39:13 +02:00
this . logger_ = l ;
}
2023-03-06 16:22:01 +02:00
public logger() {
2017-12-14 21:39:13 +02:00
return this . logger_ ;
2017-12-14 20:53:08 +02:00
}
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public on ( eventName : string , callback : Function ) {
2019-05-22 16:56:07 +02:00
return this . eventEmitter_ . on ( eventName , callback ) ;
}
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public off ( eventName : string , callback : Function ) {
2019-05-22 16:56:07 +02:00
return this . eventEmitter_ . removeListener ( eventName , callback ) ;
}
2023-03-06 16:22:01 +02:00
public 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
}
2023-03-06 16:22:01 +02:00
public setEncryptionService ( v : any ) {
2017-12-14 21:39:13 +02:00
this . encryptionService_ = v ;
}
2023-03-06 16:22:01 +02:00
public setKvStore ( v : KvStore ) {
2019-06-08 00:11:08 +02:00
this . kvStore_ = v ;
}
2023-03-06 16:22:01 +02:00
public 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
}
2023-03-06 16:22:01 +02:00
public kvStore() {
2019-06-08 00:11:08 +02:00
if ( ! this . kvStore_ ) throw new Error ( 'DecryptionWorker.kvStore_ is not set!!' ) ;
return this . kvStore_ ;
}
2023-03-06 16:22:01 +02:00
public async scheduleStart() {
2017-12-14 21:39:13 +02:00
if ( this . scheduleId_ ) return ;
2020-10-09 19:35:46 +02:00
this . scheduleId_ = shim . setTimeout ( ( ) = > {
2017-12-14 21:39:13 +02:00
this . scheduleId_ = null ;
2021-01-22 19:41:11 +02:00
void 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 ) ;
}
2023-03-06 16:22:01 +02:00
public async decryptionDisabledItems() {
2019-06-08 00:11:08 +02:00
let items = await this . kvStore ( ) . searchByPrefix ( 'decrypt:' ) ;
2020-05-21 10:14:33 +02:00
items = items . filter ( item = > item . value > this . maxDecryptionAttempts_ ) ;
items = items . map ( item = > {
2019-06-08 00:11:08 +02:00
const s = item . key . split ( ':' ) ;
return {
type_ : Number ( s [ 1 ] ) ,
id : s [ 2 ] ,
} ;
} ) ;
return items ;
}
2023-03-06 16:22:01 +02:00
public async clearDisabledItem ( typeId : string , itemId : string ) {
2019-09-19 23:51:18 +02:00
await this . kvStore ( ) . deleteValue ( ` decrypt: ${ typeId } : ${ itemId } ` ) ;
2019-06-08 00:11:08 +02:00
}
2023-03-06 16:22:01 +02:00
public async clearDisabledItems() {
2020-04-08 19:02:31 +02:00
await this . kvStore ( ) . deleteByPrefix ( 'decrypt:' ) ;
}
2023-03-06 16:22:01 +02:00
public dispatchReport ( report : any ) {
2023-06-01 13:02:36 +02:00
const action = { . . . report } ;
2018-06-10 18:43:24 +02:00
action . type = 'DECRYPTION_WORKER_SET' ;
this . dispatch ( action ) ;
}
2021-08-07 13:22:37 +02:00
private async start_ ( options : any = null ) : Promise < DecryptionResult > {
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' ) {
2020-10-30 01:37:19 +02:00
const msg = ` DecryptionWorker: cannot start because state is " ${ this . state_ } " ` ;
this . logger ( ) . debug ( msg ) ;
return { error : new Error ( msg ) } ;
2017-12-26 12:38:53 +02:00
}
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 ) {
2020-10-30 01:37:19 +02:00
const msg = 'DecryptionWorker: cannot start because no master key is currently loaded.' ;
this . logger ( ) . info ( msg ) ;
2019-05-28 23:05:11 +02:00
const ids = await MasterKey . allIds ( ) ;
2021-08-30 15:15:35 +02: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 23:05:11 +02:00
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
}
2020-10-30 01:37:19 +02:00
return { error : new Error ( msg ) } ;
2019-05-28 23:05:11 +02:00
}
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
2020-03-14 01:46:14 +02:00
const excludedIds = [ ] ;
2021-01-22 19:41:11 +02:00
const decryptedItemCounts : any = { } ;
2020-05-13 17:28:54 +02:00
let skippedItemCount = 0 ;
2017-12-14 20:53:08 +02:00
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 ) {
2021-01-22 19:41:11 +02:00
const result : ItemsThatNeedDecryptionResult = await BaseItem . itemsThatNeedDecryption ( excludedIds ) ;
2017-12-14 21:39:13 +02:00
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-04-08 02:00:01 +02:00
this . logger ( ) . debug ( ` DecryptionWorker: ${ BaseModel . modelTypeToName ( item . type_ ) } ${ item . id } : Decryption has failed more than 2 times - skipping it ` ) ;
2020-03-13 19:42:50 +02:00
this . dispatch ( { type : 'ENCRYPTION_HAS_DISABLED_ITEMS' , value : true } ) ;
2020-05-13 17:28:54 +02:00
skippedItemCount ++ ;
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 ( ) ;
2020-04-19 11:11:46 +02:00
if ( ! decryptedItemCounts [ decryptedItem . type_ ] ) decryptedItemCounts [ decryptedItem . type_ ] = 0 ;
decryptedItemCounts [ decryptedItem . type_ ] ++ ;
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
2020-04-08 02:00:01 +02:00
const downloadedButEncryptedBlobCount = await Resource . downloadedButEncryptedBlobCount ( excludedIds ) ;
2019-05-22 16:56:07 +02:00
2018-03-09 22:59:12 +02:00
this . state_ = 'idle' ;
2019-05-22 16:56:07 +02:00
2020-05-13 17:28:54 +02:00
let decryptedItemCount = 0 ;
for ( const itemType in decryptedItemCounts ) decryptedItemCount += decryptedItemCounts [ itemType ] ;
2021-08-07 13:22:37 +02:00
const finalReport : DecryptionResult = {
2020-05-13 17:28:54 +02:00
skippedItemCount : skippedItemCount ,
2020-04-19 11:11:46 +02:00
decryptedItemCounts : decryptedItemCounts ,
2020-05-13 17:28:54 +02:00
decryptedItemCount : decryptedItemCount ,
2021-08-07 13:22:37 +02:00
error : null ,
2020-05-13 17:28:54 +02:00
} ;
2023-06-01 13:02:36 +02:00
this . dispatchReport ( { . . . finalReport , state : 'idle' } ) ;
2019-05-28 23:05:11 +02:00
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 } ` ) ;
2021-01-22 19:41:11 +02:00
void this . scheduleStart ( ) ;
2019-05-22 16:56:07 +02:00
}
2020-05-13 17:28:54 +02:00
return finalReport ;
2017-12-14 19:58:10 +02:00
}
2020-02-22 13:25:16 +02:00
2021-08-07 13:22:37 +02:00
public async start ( options : any = { } ) {
2020-02-22 13:25:16 +02:00
this . startCalls_ . push ( true ) ;
2020-05-13 17:28:54 +02:00
let output = null ;
2020-02-22 13:25:16 +02:00
try {
2020-05-13 17:28:54 +02:00
output = await this . start_ ( options ) ;
2020-02-22 13:25:16 +02:00
} finally {
this . startCalls_ . pop ( ) ;
}
2020-05-13 17:28:54 +02:00
return output ;
2020-02-22 13:25:16 +02:00
}
2023-03-06 16:22:01 +02:00
public async destroy() {
2020-02-27 20:25:42 +02:00
this . eventEmitter_ . removeAllListeners ( ) ;
if ( this . scheduleId_ ) {
2020-10-09 19:35:46 +02:00
shim . clearTimeout ( this . scheduleId_ ) ;
2020-02-27 20:25:42 +02:00
this . scheduleId_ = null ;
}
this . eventEmitter_ = null ;
DecryptionWorker . instance_ = null ;
2020-02-22 13:25:16 +02:00
return new Promise ( ( resolve ) = > {
2020-10-09 19:35:46 +02:00
const iid = shim . setInterval ( ( ) = > {
2020-02-22 13:25:16 +02:00
if ( ! this . startCalls_ . length ) {
2020-10-09 19:35:46 +02:00
shim . clearInterval ( iid ) ;
2021-01-22 19:41:11 +02:00
resolve ( null ) ;
2020-02-22 13:25:16 +02:00
}
} , 100 ) ;
} ) ;
}
2017-12-14 19:58:10 +02:00
}