siyuan-note/siyuan · error

cannot change master password while encrypted notebooks are…

Error message

cannot change master password while encrypted notebooks are unlocked (DEKs in memory), lock them first

What it means

ChangeMasterPassword refuses to run while one or more encrypted notebooks are unlocked, because their DEKs are in memory. Switching the KEK under cached DEKs would leave the in-memory cache inconsistent with the newly wrapped envelopes on disk, so the operation is aborted and the user must lock all encrypted notebooks first.

Solutions

  1. Lock every encrypted notebook (LockEncryptedBox / UI lock) so DEKs are zeroed and removed from the cache, then retry the password change
  2. Restart the kernel/app to guarantee a clean in-memory state if a stale DEK is suspected
  3. Close background jobs/plugins that may re-mount encrypted notebooks during the change

Example fix

// before
await lockEncryptedBoxes(); await changeMasterPassword(oldPw, newPw)
// after
for (const box of encryptedBoxes) { if (!isBoxLocked(box.id)) await lockBox(box.id) }
await changeMasterPassword(oldPw, newPw)
Defensive patterns

Strategy: validation

Validate before calling

if (encryptedBoxes.some(b => !b.locked)) { throw new Error("lock all encrypted notebooks before changing the master password") }

Try / catch

try { await changeMasterPassword(oldPw, newPw) } catch (e) { if (e.message.includes("lock them first")) { await lockAllEncryptedBoxes(); retryChange() } }

Prevention

When it happens

Trigger: Calling ChangeMasterPassword while cachedDEKs is non-empty — i.e. any encrypted notebook is in an unlocked/mounted state, or a previous lock operation failed to evict its DEK.

Common situations: User attempts a master-password change from settings while an encrypted notebook is open; a background job or plugin holds a mounted encrypted notebook; a stale DEK left by a failed unlock/lock cycle.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/c0cab84244bd478f. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/crypto.go:1721

//	Phase 2: 切换全局 verifier
//	Phase 3: 写入各 box conf + backup
//	Phase 4: 清除 manifest
//
// 注意:必须在所有加密笔记本都已 Unmount 的状态下调用(DEK 不在内存),否则新旧 KEK 切换会让缓存与磁盘不一致。
func ChangeMasterPassword(oldPassword, newPassword string) error {
	if len(newPassword) == 0 {
		return errors.New("new password must not be empty")
	}

	notebookCryptoMu.Lock()
	defer notebookCryptoMu.Unlock()

	// 改密期间不能有已 Mount 的加密笔记本(DEK 在内存),否则新旧 KEK 切换会让缓存与磁盘不一致
	cachedDEKsLock.RLock()
	dekCount := len(cachedDEKs)
	cachedDEKsLock.RUnlock()
	if dekCount > 0 {
		return errors.New("cannot change master password while encrypted notebooks are unlocked (DEKs in memory), lock them first")
	}

	oldKEK, err := deriveKEK(oldPassword)
	if err != nil {
		return err
	}
	defer zeroAndClear(oldKEK)

	nc := currentNotebookCrypto()

	params, validErr := util.ValidateArgon2Params(nc.KDFParams)
	if validErr != nil {
		return validErr
	}
	newKEK := util.DeriveKey(newPassword, nc.MasterSalt, params)
	defer zeroAndClear(newKEK)
	newHistoryKEKs, err := rewrapHistoryKEKs(oldKEK, newKEK, nc.HistoryKEKs)
	if err != nil {

View on GitHub (pinned to 9f775e8a12)