import uuid from '../../uuid'; import Setting from '../../models/Setting'; import EncryptionService from '../e2ee/EncryptionService'; import { MasterKeyEntity } from '../e2ee/types'; import { localSyncInfo, saveLocalSyncInfo } from '../synchronizer/syncInfoUtils'; export interface DecryptedNoteLockKey { id: string; plainText: string; } // Importers that predate note lock skip this file, since they only read .md files. export const noteLockKeyFileName = 'note_lock_key.json'; export default class NoteLockKey { public static instance_: NoteLockKey = null; private constructor(private encryptionService_: EncryptionService = EncryptionService.instance()) {} public static instance() { if (!this.instance_) { this.instance_ = new NoteLockKey(); } return this.instance_; } public static destroyInstance() { this.instance_ = null; } public load(): MasterKeyEntity { return localSyncInfo().noteLockKey; } public save(o: MasterKeyEntity): MasterKeyEntity { const key = { ...o }; if (!key.id) { key.id = uuid.create(); key.created_time = Date.now(); } key.updated_time = Date.now(); const syncInfo = localSyncInfo(); syncInfo.noteLockKey = key; // Only a key migration replaces the sync migration id, so a reset keeps the lineage it belongs to. if (!syncInfo.syncMigrationId) syncInfo.syncMigrationId = uuid.create(); saveLocalSyncInfo(syncInfo); return key; } public async create(password: string) { if (this.load()) throw new Error('Note lock key already exists'); const key = await this.encryptionService_.generateMasterKey(password); // A key may have arrived through sync while the new one was being generated - keep it, // since notes may already be locked with it on another device. if (this.load()) throw new Error('Note lock key already exists'); return this.save(key); } // Rotate through NoteLockSession.reset() rather than calling this directly, so the session locks // and drops the old key as part of the rotation. public async reset(password: string) { const generated = await this.encryptionService_.generateMasterKey(password); // Read once the key is ready, so a sync that finished meanwhile counts; a second reset before that sync keeps the first backup. if (!Setting.value('noteLock.keyIdToReset')) Setting.setValue('noteLock.keyIdToReset', this.load()?.id ?? ''); const key = this.save(generated); // Lets the next sync push the new key to the target instead of adopting the old one back. Setting.setValue('noteLock.passwordReset', true); return key; } public async changePassword(currentPassword: string, newPassword: string) { const key = this.load(); if (!key) throw new Error('Note lock key has not been created'); return this.save(await this.encryptionService_.reencryptMasterKey(key, currentPassword, newPassword)); } public needsUpgrade() { const key = this.load(); return !!key && !!this.encryptionService_.masterKeysThatNeedUpgrading([key]).length; } public async upgrade(password: string) { const key = this.load(); if (!key) throw new Error('Note lock key has not been created'); if (!this.needsUpgrade()) return key; return this.changePassword(password, password); } public async decrypt(password: string, key: MasterKeyEntity = null): Promise { if (!key) key = this.load(); if (!key) throw new Error('Note lock key has not been created'); if (!key.id) throw new Error('Note lock key does not have an ID'); const plainText = await this.encryptionService_.decryptMasterKeyContent(key, password); return { id: key.id, plainText }; } }