一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
100 行
3.6 KiB
TypeScript
100 行
3.6 KiB
TypeScript
import NoteLockKey, { DecryptedNoteLockKey } from './NoteLockKey';
|
|
import eventManager, { EventName } from '../../eventManager';
|
|
import { noteLockKeyConflict } from '../synchronizer/syncInfoUtils';
|
|
import { _ } from '../../locale';
|
|
|
|
export default class NoteLockSession {
|
|
|
|
public static instance_: NoteLockSession = null;
|
|
|
|
private key_: DecryptedNoteLockKey = null;
|
|
private lockGeneration_ = 0;
|
|
private rotating_ = false;
|
|
private migrating_ = false;
|
|
|
|
private constructor(private noteLockKey_: NoteLockKey = NoteLockKey.instance()) {}
|
|
|
|
public static instance() {
|
|
if (!this.instance_) {
|
|
this.instance_ = new NoteLockSession();
|
|
}
|
|
return this.instance_;
|
|
}
|
|
|
|
public static destroyInstance() {
|
|
// lock() bumps the generation so a mid-decrypt unlock() cannot repopulate a torn-down session.
|
|
this.instance_?.lock();
|
|
this.instance_ = null;
|
|
}
|
|
|
|
// Re-checks state after the await so a lock, reset, synced change or teardown that raced in
|
|
// can't install a stale key. A reset that starts mid-decrypt also locks, so the generation
|
|
// check covers it.
|
|
public async unlock(password: string) {
|
|
if (this.rotating_) throw new Error('Cannot unlock: a note lock key reset is in progress');
|
|
if (this.migrating_) throw new Error(_('This note cannot be read while a note lock key migration is in progress.'));
|
|
const generation = this.lockGeneration_;
|
|
const decrypted = await this.noteLockKey_.decrypt(password);
|
|
if (this.lockGeneration_ !== generation) throw new Error('Cannot unlock: the session was locked while unlocking');
|
|
if (this.noteLockKey_.load()?.id !== decrypted.id) throw new Error('Cannot unlock: the note lock key changed while unlocking');
|
|
this.key_ = decrypted;
|
|
eventManager.emit(EventName.NoteLockSessionChange, { unlocked: true });
|
|
}
|
|
|
|
public lock() {
|
|
this.lockGeneration_++;
|
|
const wasUnlocked = !!this.key_;
|
|
this.key_ = null;
|
|
if (wasUnlocked) eventManager.emit(EventName.NoteLockSessionChange, { unlocked: false });
|
|
}
|
|
|
|
// Blocks unlock during rotation so the still-persisted old key can't be unlocked before the new one is saved.
|
|
public async reset(password: string) {
|
|
if (this.rotating_) throw new Error('A note lock key reset is already in progress');
|
|
// The migration would adopt the target key over the new one when it ends.
|
|
if (this.migrating_) throw new Error(_('The note lock password cannot be reset while a note lock key migration is in progress.'));
|
|
this.rotating_ = true;
|
|
this.lock();
|
|
try {
|
|
return await this.noteLockKey_.reset(password);
|
|
} finally {
|
|
this.lock();
|
|
this.rotating_ = false;
|
|
}
|
|
}
|
|
|
|
// Locked notes stay closed in every window while a migration run re-encrypts them.
|
|
public setMigrating(migrating: boolean) {
|
|
this.migrating_ = migrating;
|
|
if (migrating) this.lock();
|
|
}
|
|
|
|
public isMigrating() {
|
|
return this.migrating_;
|
|
}
|
|
|
|
private lockIfKeyChanged_() {
|
|
if (this.key_ && this.key_.id !== this.noteLockKey_.load()?.id) this.lock();
|
|
}
|
|
|
|
public isUnlocked() {
|
|
this.lockIfKeyChanged_();
|
|
return !!this.key_;
|
|
}
|
|
|
|
public decryptedKey(): DecryptedNoteLockKey {
|
|
this.lockIfKeyChanged_();
|
|
if (!this.key_) throw new Error('Note lock session is locked');
|
|
return {
|
|
id: this.key_.id,
|
|
plainText: this.key_.plainText,
|
|
};
|
|
}
|
|
|
|
// A lock clears key_ but not the persisted key, so this only fails on a real rotation, or one in progress.
|
|
// The key parked for migration is allowed too: migration re-encrypts to it before it is adopted.
|
|
public assertCanEncryptWith(keyId: string) {
|
|
if (this.rotating_ || (this.noteLockKey_.load()?.id !== keyId && noteLockKeyConflict()?.noteLockKey.id !== keyId)) throw new Error('Note lock key changed during operation');
|
|
}
|
|
}
|