{"record":{"id":"e5d14cb98fa1d847","repo":"siyuan-note/siyuan","slug":"encrypted-notebook-locked-please-unlock-it-first","errorCode":null,"errorMessage":"encrypted notebook locked, please unlock it first","messagePattern":"encrypted notebook locked, please unlock it first","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/mount.go","lineNumber":462,"sourceCode":"\t\t\tCheckUpdate(true)\n\t\t}()\n\t}\n\n\tif !gulu.File.IsDir(localPath) {\n\t\treturn false, errors.New(\"can not open file, just support open folder only\")\n\t}\n\n\tfor _, box := range Conf.GetOpenedBoxes() {\n\t\tif box.ID == boxID {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\t// 加密笔记本必须先通过 UnlockBox 解出 DEK，否则拒绝挂载。Mount 本身不接收密码，\n\t// 前端流程为：先调 /api/notebook/unlockBox 解锁，再调 openNotebook 挂载。\n\t// 使用 IsEncryptedBox 统一判定（含 backup fallback，不依赖 conf 完整性）。\n\tif IsEncryptedBox(boxID) && !IsBoxUnlocked(boxID) {\n\t\treturn false, errors.New(\"encrypted notebook locked, please unlock it first\")\n\t}\n\n\tbox := &Box{ID: boxID}\n\tboxConf := box.GetConf()\n\tboxConf.Closed = false\n\tif err := box.SaveConf(boxConf); err != nil {\n\t\tlogging.LogErrorf(\"save box conf [%s] failed: %s\", boxID, err)\n\t}\n\tif boxConf.Encrypted {\n\t\tmarkRuntimeEncryptedBox(boxID)\n\t\tmountedEncryptedBoxes.Store(boxID, true)\n\t}\n\tif _, ensureErr := EnsureBoxDoc(boxID); nil != ensureErr {\n\t\tlogging.LogErrorf(\"ensure box document [%s] failed: %s\", boxID, ensureErr)\n\t}\n\n\t// 缓存根一级的文档树展开\n\tfiles, _, _ := ListDocTree(box.ID, \"/\", util.SortModeUnassigned, false, false, Conf.FileTree.MaxListCount)","sourceCodeStart":444,"sourceCodeEnd":480,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/mount.go#L444-L480","documentation":"Returned by mountBox (kernel/model/mount.go:462) when IsEncryptedBox(boxID) is true but IsBoxUnlocked(boxID) is false. Encrypted notebooks hold their DEK only in memory after an explicit UnlockBox call supplies the password; Mount itself takes no password, so it refuses to open a still-locked encrypted notebook.","triggerScenarios":"Calling /api/notebook/openNotebook for an encrypted notebook before calling /api/notebook/unlockBox with the correct password, or after a kernel restart that cleared the in-memory DEK.","commonSituations":"Kernel was restarted (DEK is volatile), the user cleared the unlock from the UI, or a script orchestrates open without the preceding unlock step.","solutions":["Call UnlockBox (model.UnlockBox / /api/notebook/unlockBox) with the master password first, then Mount.","If the password is lost, use the backup recovery flow; otherwise the notebook cannot be opened.","Re-unlock after every kernel restart — the unlocked state does not persist."],"exampleFix":"// before\nmodel.Mount(boxID) // encrypted but not unlocked -> error\n// after\nif err := model.UnlockBox(boxID, password); err != nil {\n    return err\n}\n_, err := model.Mount(boxID)","handlingStrategy":"validation","validationCode":"if model.IsEncryptedBox(boxID) && !model.IsBoxUnlocked(boxID) {\n    if err := model.UnlockBox(boxID, password); err != nil {\n        return err\n    }\n}\nreturn model.Mount(boxID)","typeGuard":"func needsUnlock(boxID string) bool {\n    return model.IsEncryptedBox(boxID) && !model.IsBoxUnlocked(boxID)\n}","tryCatchPattern":null,"preventionTips":["Always pair unlock-before-open for encrypted notebooks in client flows.","Treat the unlocked state as session-scoped — re-establish it after restarts.","Never cache the password; only the DEK is held in memory."],"tags":["notebook","encryption","auth","mount"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}