siyuan-note/siyuan · error

encrypted notebook locked, please unlock it first

Error message

encrypted notebook locked, please unlock it first

What it means

Encrypted notebooks store their data encrypted under a DEK wrapped by the user's password. Mount never receives a password, so it requires the box to already be unlocked via UnlockBox (/api/notebook/unlockBox); IsEncryptedBox (including backup fallback) is true while IsBoxUnlocked is false, and mounting is refused. The intended frontend flow is: unlockBox first, then openNotebook.

Solutions

  1. Call /api/notebook/unlockBox with the notebook password first, then call openNotebook.
  2. Check lock state beforehand (IsEncryptedBox/IsBoxUnlocked equivalents via API) and route to the unlock UI when locked.
  3. After a kernel restart, expect all encrypted notebooks to be locked again and re-run the unlock flow.
  4. If the password is forgotten, use the documented recovery flow rather than bypassing the lock.

Example fix

// before: mounting an encrypted notebook directly
await fetchPost("/api/notebook/openNotebook", { notebook: boxID });
// -> encrypted notebook locked, please unlock it first

// after: unlock before mount
await fetchPost("/api/notebook/unlockBox", { notebook: boxID, password });
await fetchPost("/api/notebook/openNotebook", { notebook: boxID });
Defensive patterns

Strategy: try-catch

Validate before calling

// query state before mounting
const boxes = (await fetchPost("/api/notebook/lsNotebooks", {})).data.notebooks;
// if the notebook is encrypted and not yet unlocked, unlockBox must run first

async function ensureOpen(boxID: string, password: string): Promise<void> {
  await fetchPost("/api/notebook/unlockBox", { notebook: boxID, password });
  await fetchPost("/api/notebook/openNotebook", { notebook: boxID });
}

Try / catch

try {
  await fetchPost("/api/notebook/openNotebook", { notebook: boxID });
} catch (e) {
  if (String(e).includes("encrypted notebook locked")) {
    await promptUnlockDialog(boxID); // collects password, calls unlockBox, then openNotebook
  }
}

Prevention

When it happens

Trigger: Calling /api/notebook/openNotebook (Mount) on an encrypted notebook after kernel restart or before ever entering the password; calling Mount concurrently with a flow that re-locked the box; a plugin opening the notebook directly instead of going through the unlock dialog.

Common situations: Automated scripts or plugins that call openNotebook without knowing the notebook is encrypted; session loss after kernel reboot (DEK is memory-only); attempting to mount an encrypted notebook whose unlock was cancelled by the user.

Related errors


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

Appendix: source

Thrown at kernel/model/mount.go:550

			CheckUpdate(true)
		}()
	}

	if !gulu.File.IsDir(localPath) {
		return false, errors.New("can not open file, just support open folder only")
	}

	for _, box := range Conf.GetOpenedBoxes() {
		if box.ID == boxID {
			return true, nil
		}
	}

	// 加密笔记本必须先通过 UnlockBox 解出 DEK,否则拒绝挂载。Mount 本身不接收密码,
	// 前端流程为:先调 /api/notebook/unlockBox 解锁,再调 openNotebook 挂载。
	// 使用 IsEncryptedBox 统一判定(含 backup fallback,不依赖 conf 完整性)。
	if IsEncryptedBox(boxID) && !IsBoxUnlocked(boxID) {
		return false, errors.New("encrypted notebook locked, please unlock it first")
	}

	box := &Box{ID: boxID}
	boxConf := box.GetConf()
	boxConf.Closed = false
	if err := box.SaveConf(boxConf); err != nil {
		logging.LogErrorf("save box conf [%s] failed: %s", boxID, err)
	}
	if boxConf.Encrypted {
		markRuntimeEncryptedBox(boxID)
		mountedEncryptedBoxes.Store(boxID, true)
	}
	if _, ensureErr := EnsureBoxDoc(boxID); nil != ensureErr {
		logging.LogErrorf("ensure box document [%s] failed: %s", boxID, ensureErr)
	}

	// 缓存根一级的文档树展开
	files, _, _ := ListDocTree(box.ID, "/", util.SortModeUnassigned, false, false, Conf.FileTree.MaxListCount)

View on GitHub (pinned to 9f775e8a12)