1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Mobile,Desktop,CLI: Fixes #10856: Decrypt master keys only as needed (#10990)

This commit is contained in:
Henry Heino 2024-09-07 03:56:13 -07:00 committed by GitHub
parent ac2258769a
commit 0bfa28d795
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 78 additions and 25 deletions

View File

@ -207,7 +207,13 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
setTimeLocale(Setting.value('locale')); setTimeLocale(Setting.value('locale'));
} }
if ((action.type === 'SETTING_UPDATE_ONE' && (action.key.indexOf('encryption.') === 0)) || (action.type === 'SETTING_UPDATE_ALL')) { // Like the desktop and CLI apps, we run this whenever the sync target properties change.
// Previously, this only ran when encryption was enabled/disabled. However, after fetching
// a new key, this needs to run and so we run it when the sync target info changes.
if (
(action.type === 'SETTING_UPDATE_ONE' && (action.key === 'syncInfoCache' || action.key.startsWith('encryption.')))
|| action.type === 'SETTING_UPDATE_ALL'
) {
await loadMasterKeysFromSettings(EncryptionService.instance()); await loadMasterKeysFromSettings(EncryptionService.instance());
void DecryptionWorker.instance().scheduleStart(); void DecryptionWorker.instance().scheduleStart();
const loadedMasterKeyIds = EncryptionService.instance().loadedMasterKeyIds(); const loadedMasterKeyIds = EncryptionService.instance().loadedMasterKeyIds();

View File

@ -506,6 +506,13 @@ export default class BaseItem extends BaseModel {
masterKeyId: share && share.master_key_id ? share.master_key_id : '', masterKeyId: share && share.master_key_id ? share.master_key_id : '',
}); });
} catch (error) { } catch (error) {
if (error.code === 'masterKeyNotLoaded' && error.masterKeyId) {
this.dispatch?.({
type: 'MASTERKEY_ADD_NOT_LOADED',
id: error.masterKeyId,
});
}
const msg = [`Could not encrypt item ${item.id}`]; const msg = [`Could not encrypt item ${item.id}`];
if (error && error.message) msg.push(error.message); if (error && error.message) msg.push(error.message);
const newError = new Error(msg.join(': ')); const newError = new Error(msg.join(': '));

View File

@ -52,6 +52,12 @@ export interface EncryptOptions {
masterKeyId?: string; masterKeyId?: string;
} }
type GetPasswordCallback = ()=> string|Promise<string>;
interface EncryptedMasterKey {
updatedTime: number;
decrypt: ()=> Promise<void>;
}
export default class EncryptionService { export default class EncryptionService {
public static instance_: EncryptionService = null; public static instance_: EncryptionService = null;
@ -73,7 +79,8 @@ export default class EncryptionService {
// So making the block 10 times smaller make it 100 times faster! So for now using 5KB. This can be // So making the block 10 times smaller make it 100 times faster! So for now using 5KB. This can be
// changed easily since the chunk size is incorporated into the encrypted data. // changed easily since the chunk size is incorporated into the encrypted data.
private chunkSize_ = 5000; private chunkSize_ = 5000;
private decryptedMasterKeys_: Record<string, DecryptedMasterKey> = {}; private encryptedMasterKeys_: Map<string, EncryptedMasterKey> = new Map();
private decryptedMasterKeys_: Map<string, DecryptedMasterKey> = new Map();
public defaultEncryptionMethod_ = EncryptionMethod.SJCL1a; // public because used in tests public defaultEncryptionMethod_ = EncryptionMethod.SJCL1a; // public because used in tests
private defaultMasterKeyEncryptionMethod_ = EncryptionMethod.SJCL4; private defaultMasterKeyEncryptionMethod_ = EncryptionMethod.SJCL4;
@ -96,7 +103,7 @@ export default class EncryptionService {
} }
public loadedMasterKeysCount() { public loadedMasterKeysCount() {
return Object.keys(this.decryptedMasterKeys_).length; return this.loadedMasterKeyIds().length;
} }
public chunkSize() { public chunkSize() {
@ -123,41 +130,78 @@ export default class EncryptionService {
} }
public isMasterKeyLoaded(masterKey: MasterKeyEntity) { public isMasterKeyLoaded(masterKey: MasterKeyEntity) {
const d = this.decryptedMasterKeys_[masterKey.id]; if (this.encryptedMasterKeys_.get(masterKey.id)) {
return true;
}
const d = this.decryptedMasterKeys_.get(masterKey.id);
if (!d) return false; if (!d) return false;
return d.updatedTime === masterKey.updated_time; return d.updatedTime === masterKey.updated_time;
} }
public async loadMasterKey(model: MasterKeyEntity, password: string, makeActive = false) { public async loadMasterKey(model: MasterKeyEntity, getPassword: string|GetPasswordCallback, makeActive = false) {
if (!model.id) throw new Error('Master key does not have an ID - save it first'); if (!model.id) throw new Error('Master key does not have an ID - save it first');
logger.info(`Loading master key: ${model.id}. Make active:`, makeActive); const loadKey = async () => {
logger.info(`Loading master key: ${model.id}. Make active:`, makeActive);
this.decryptedMasterKeys_[model.id] = { const password = typeof getPassword === 'string' ? getPassword : (await getPassword());
plainText: await this.decryptMasterKeyContent(model, password), if (!password) {
updatedTime: model.updated_time, logger.info(`Loading master key ${model.id} failed. No valid password found.`);
} else {
try {
this.decryptedMasterKeys_.set(model.id, {
plainText: await this.decryptMasterKeyContent(model, password),
updatedTime: model.updated_time,
});
if (makeActive) this.setActiveMasterKeyId(model.id);
} catch (error) {
logger.warn(`Cannot load master key ${model.id}. Invalid password?`, error);
}
}
this.encryptedMasterKeys_.delete(model.id);
}; };
if (makeActive) this.setActiveMasterKeyId(model.id); if (!makeActive) {
this.encryptedMasterKeys_.set(model.id, {
decrypt: loadKey,
updatedTime: model.updated_time,
});
} else {
await loadKey();
}
} }
public unloadMasterKey(model: MasterKeyEntity) { public unloadMasterKey(model: MasterKeyEntity) {
delete this.decryptedMasterKeys_[model.id]; this.decryptedMasterKeys_.delete(model.id);
this.encryptedMasterKeys_.delete(model.id);
} }
public loadedMasterKey(id: string) { public async loadedMasterKey(id: string) {
if (!this.decryptedMasterKeys_[id]) { const cachedKey = this.decryptedMasterKeys_.get(id);
if (cachedKey) return cachedKey;
const decryptCallback = this.encryptedMasterKeys_.get(id);
if (decryptCallback) {
// TODO: Handle invalid password errors?
await decryptCallback.decrypt();
}
const key = this.decryptedMasterKeys_.get(id);
if (!key) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const error: any = new Error(`Master key is not loaded: ${id}`); const error: any = new Error(`Master key is not loaded: ${id}`);
error.code = 'masterKeyNotLoaded'; error.code = 'masterKeyNotLoaded';
error.masterKeyId = id; error.masterKeyId = id;
throw error; throw error;
} }
return this.decryptedMasterKeys_[id]; return key;
} }
public loadedMasterKeyIds() { public loadedMasterKeyIds() {
return Object.keys(this.decryptedMasterKeys_); return [...this.decryptedMasterKeys_.keys(), ...this.encryptedMasterKeys_.keys()];
} }
public fsDriver() { public fsDriver() {
@ -430,7 +474,7 @@ export default class EncryptionService {
const method = options.encryptionMethod; const method = options.encryptionMethod;
const masterKeyId = options.masterKeyId ? options.masterKeyId : this.activeMasterKeyId(); const masterKeyId = options.masterKeyId ? options.masterKeyId : this.activeMasterKeyId();
const masterKeyPlainText = this.loadedMasterKey(masterKeyId).plainText; const masterKeyPlainText = (await this.loadedMasterKey(masterKeyId)).plainText;
const header = { const header = {
encryptionMethod: method, encryptionMethod: method,
@ -465,7 +509,7 @@ export default class EncryptionService {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const header: any = await this.decodeHeaderSource_(source); const header: any = await this.decodeHeaderSource_(source);
const masterKeyPlainText = this.loadedMasterKey(header.masterKeyId).plainText; const masterKeyPlainText = (await this.loadedMasterKey(header.masterKeyId)).plainText;
let doneSize = 0; let doneSize = 0;

View File

@ -142,14 +142,10 @@ export async function loadMasterKeysFromSettings(service: EncryptionService) {
const mk = masterKeys[i]; const mk = masterKeys[i];
if (service.isMasterKeyLoaded(mk)) continue; if (service.isMasterKeyLoaded(mk)) continue;
const password = await findMasterKeyPassword(service, mk); await service.loadMasterKey(mk, async () => {
if (!password) continue; const password = await findMasterKeyPassword(service, mk);
return password;
try { }, activeMasterKeyId === mk.id);
await service.loadMasterKey(mk, password, activeMasterKeyId === mk.id);
} catch (error) {
logger.warn(`Cannot load master key ${mk.id}. Invalid password?`, error);
}
} }
logger.info(`Loaded master keys: ${service.loadedMasterKeysCount()}`); logger.info(`Loaded master keys: ${service.loadedMasterKeysCount()}`);