文件
mynode/packages/lib/services/noteLock/NoteLockSession.test.ts
chen 301194963b
一些检查失败了
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
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

260 行
9.4 KiB
TypeScript

import { afterAllCleanUp, encryptionService, setupDatabaseAndSynchronizer, switchClient, synchronizerStart } from '../../testing/test-utils';
import Setting from '../../models/Setting';
import EncryptionService from '../e2ee/EncryptionService';
import { localSyncInfo, saveLocalSyncInfo } from '../synchronizer/syncInfoUtils';
import NoteLockKey, { DecryptedNoteLockKey } from './NoteLockKey';
import NoteLockSession from './NoteLockSession';
import NoteLockService from './NoteLockService';
import eventManager, { EventName, NoteLockSessionChangeEvent } from '../../eventManager';
const unlockedSession = async (password = '123456') => {
const session = NoteLockSession.instance();
await NoteLockKey.instance().create(password);
await session.unlock(password);
return session;
};
const changeSyncedKeyId = (id: string) => {
const syncInfo = localSyncInfo();
syncInfo.noteLockKey = { ...syncInfo.noteLockKey, id };
saveLocalSyncInfo(syncInfo);
};
// Gates NoteLockKey.decrypt() so a lock, reset or teardown can be forced to land while an unlock is
// still awaiting. Returns a function that lets the decryption resolve.
const gateDecrypt = (result?: DecryptedNoteLockKey) => {
let release: ()=> void = () => {};
const gate = new Promise<void>(resolve => { release = resolve; });
const key = NoteLockKey.instance();
const realDecrypt = key.decrypt.bind(key);
jest.spyOn(key, 'decrypt').mockImplementation(async password => {
await gate;
return result ?? realDecrypt(password);
});
return release;
};
// Gates generateMasterKey() so a reset can be held mid-rotation, with the old key still persisted,
// while another unlock or reset races it. Returns a function that lets the rotation resolve.
const gateKeyGeneration = () => {
let release: ()=> void = () => {};
const gate = new Promise<void>(resolve => { release = resolve; });
const encryption = EncryptionService.instance();
const realGenerate = encryption.generateMasterKey.bind(encryption);
jest.spyOn(encryption, 'generateMasterKey').mockImplementation(async (password, options) => {
await gate;
return realGenerate(password, options);
});
return release;
};
describe('NoteLockSession', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await setupDatabaseAndSynchronizer(2);
await switchClient(1);
NoteLockService.destroyInstance();
NoteLockSession.destroyInstance();
NoteLockKey.destroyInstance();
EncryptionService.instance_ = encryptionService();
});
afterAll(async () => {
await afterAllCleanUp();
});
it('should stay locked after create and reset until explicitly unlocked', async () => {
const session = NoteLockSession.instance();
const firstKey = await NoteLockKey.instance().create('123456');
expect(session.isUnlocked()).toBe(false);
await session.unlock('123456');
expect(session.isUnlocked()).toBe(true);
const secondKey = await session.reset('654321');
expect(secondKey.id).not.toBe(firstKey.id);
expect(session.isUnlocked()).toBe(false);
await session.unlock('654321');
expect(session.isUnlocked()).toBe(true);
});
it('should reject an incorrect password and stay locked', async () => {
const session = NoteLockSession.instance();
await NoteLockKey.instance().create('123456');
await expect(session.unlock('wrong password')).rejects.toThrow();
expect(session.isUnlocked()).toBe(false);
});
it('should lock and clear the key', async () => {
const session = await unlockedSession();
session.lock();
expect(session.isUnlocked()).toBe(false);
expect(() => session.decryptedKey()).toThrow('Note lock session is locked');
});
it('should lock when the synced key changes', async () => {
const session = await unlockedSession();
changeSyncedKeyId('0123456789abcdef0123456789abcdef');
expect(session.isUnlocked()).toBe(false);
});
it('should stay locked when locked while an unlock is in flight', async () => {
const session = NoteLockSession.instance();
await NoteLockKey.instance().create('123456');
const release = gateDecrypt();
const unlocking = session.unlock('123456');
session.lock();
release();
await expect(unlocking).rejects.toThrow('locked while unlocking');
expect(session.isUnlocked()).toBe(false);
});
it('should not repopulate the key when destroyed mid-unlock', async () => {
const session = NoteLockSession.instance();
await NoteLockKey.instance().create('123456');
const release = gateDecrypt();
const unlocking = session.unlock('123456');
NoteLockSession.destroyInstance();
release();
await expect(unlocking).rejects.toThrow('locked while unlocking');
expect(session.isUnlocked()).toBe(false);
});
it('should not install a key that was replaced by sync while unlocking', async () => {
const session = NoteLockSession.instance();
const originalKey = await NoteLockKey.instance().create('123456');
// Generation is unchanged here, but the loaded id no longer matches the decrypted one.
const release = gateDecrypt({ id: originalKey.id, plainText: 'plaintext' });
const unlocking = session.unlock('123456');
changeSyncedKeyId('0123456789abcdef0123456789abcdef');
release();
await expect(unlocking).rejects.toThrow('key changed while unlocking');
expect(session.isUnlocked()).toBe(false);
});
it('should keep the sync migration id and the first replaced key id through password resets', async () => {
const session = NoteLockSession.instance();
const firstKey = await NoteLockKey.instance().create('123456');
const syncMigrationId = localSyncInfo().syncMigrationId;
expect(syncMigrationId).toHaveLength(32);
expect(Setting.value('noteLock.passwordReset')).toBe(false);
await session.reset('654321');
expect(localSyncInfo().syncMigrationId).toBe(syncMigrationId);
expect(Setting.value('noteLock.passwordReset')).toBe(true);
expect(Setting.value('noteLock.keyIdToReset')).toBe(firstKey.id);
// A second reset before a sync still has to replace the key the sync target holds.
await session.reset('999999');
expect(Setting.value('noteLock.keyIdToReset')).toBe(firstKey.id);
});
it('should not install a key that was reset while unlocking', async () => {
const session = NoteLockSession.instance();
const originalKey = await NoteLockKey.instance().create('123456');
const release = gateDecrypt({ id: originalKey.id, plainText: 'plaintext' });
const unlocking = session.unlock('123456');
await session.reset('654321');
release();
await expect(unlocking).rejects.toThrow('locked while unlocking');
expect(session.isUnlocked()).toBe(false);
});
it('should reject an old-password unlock that races a reset mid-rotation', async () => {
const session = await unlockedSession();
const releaseRotation = gateKeyGeneration();
const resetting = session.reset('654321');
await expect(session.unlock('123456')).rejects.toThrow('reset is in progress');
releaseRotation();
await resetting;
expect(session.isUnlocked()).toBe(false);
});
it('should reject a reset that overlaps another rotation', async () => {
const session = await unlockedSession();
const releaseRotation = gateKeyGeneration();
const firstReset = session.reset('654321');
await expect(session.reset('abcdef')).rejects.toThrow('A note lock key reset is already in progress');
releaseRotation();
await firstReset;
expect(session.isUnlocked()).toBe(false);
});
it('should emit a session change event on unlock and on an effective lock only', async () => {
const events: NoteLockSessionChangeEvent[] = [];
const listener = (event: NoteLockSessionChangeEvent) => events.push(event);
eventManager.on(EventName.NoteLockSessionChange, listener);
try {
const session = NoteLockSession.instance();
await NoteLockKey.instance().create('123456');
session.lock();
expect(events).toEqual([]);
await session.unlock('123456');
expect(events).toEqual([{ unlocked: true }]);
session.lock();
session.lock();
expect(events).toEqual([{ unlocked: true }, { unlocked: false }]);
await session.unlock('123456');
await session.reset('654321');
expect(events.slice(2)).toEqual([{ unlocked: true }, { unlocked: false }]);
await session.unlock('654321');
NoteLockSession.destroyInstance();
expect(events.slice(4)).toEqual([{ unlocked: true }, { unlocked: false }]);
} finally {
eventManager.off(EventName.NoteLockSessionChange, listener);
}
});
it('should emit a lock event when the synced key changes', async () => {
const session = await unlockedSession();
const events: NoteLockSessionChangeEvent[] = [];
const listener = (event: NoteLockSessionChangeEvent) => events.push(event);
eventManager.on(EventName.NoteLockSessionChange, listener);
try {
changeSyncedKeyId('0123456789abcdef0123456789abcdef');
expect(session.isUnlocked()).toBe(false);
expect(events).toEqual([{ unlocked: false }]);
} finally {
eventManager.off(EventName.NoteLockSessionChange, listener);
}
});
it('should sync the encrypted note lock key without auto-unlocking or loading it into the E2EE registry', async () => {
const createdKey = await NoteLockKey.instance().create('123456');
NoteLockService.destroyInstance();
NoteLockSession.destroyInstance();
NoteLockKey.destroyInstance();
await synchronizerStart();
await switchClient(2);
EncryptionService.instance_ = encryptionService();
await synchronizerStart();
expect(NoteLockKey.instance().load()).toEqual(createdKey);
expect(NoteLockSession.instance().isUnlocked()).toBe(false);
expect(EncryptionService.instance().loadedMasterKeysCount()).toBe(0);
});
});