{"record":{"id":"fb6dee2082d3a021","repo":"siyuan-note/siyuan","slug":"please-unlock-the-encrypted-notebook-first-fb6dee","errorCode":null,"errorMessage":"Please unlock the encrypted notebook first","messagePattern":"Please unlock the encrypted notebook first","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/export.go","lineNumber":583,"sourceCode":"// 用于文档级导出入口的统一 guard，避免未解锁时读出密文或空结果。\nfunc exportLockedByBlockID(id string) bool {\n\tbt := getExportBlockTree(id)\n\tif nil == bt {\n\t\treturn false // 找不到块树，交给后续流程处理\n\t}\n\treturn IsEncryptedBox(bt.BoxID) && !IsBoxUnlocked(bt.BoxID)\n}\n\n// withExportReadLockByBlockID 由 blockID 反查 boxID，若属于加密笔记本则全程持读锁执行 fn。\n// 持锁期间 LockBox（自动锁定）会阻塞等待，避免操作中途清 DEK/删导出目录导致部分明文写出。\n// 普通笔记本或块树不存在时直接执行 fn。嵌套调用安全：sync.RWMutex.RLock 可重入。\nfunc withExportReadLockByBlockID(id string, fn func() error) error {\n\tbt := getExportBlockTree(id)\n\tif nil == bt || !IsEncryptedBox(bt.BoxID) {\n\t\treturn fn()\n\t}\n\tif !IsBoxUnlocked(bt.BoxID) {\n\t\treturn errors.New(Conf.Language(314))\n\t}\n\tHoldBoxReadLock(bt.BoxID)\n\tdefer ReleaseBoxReadLock(bt.BoxID)\n\tif _, dekErr := GetDEKIfUnlocked(bt.BoxID); dekErr != nil {\n\t\treturn errors.New(Conf.Language(314))\n\t}\n\treturn fn()\n}\n\nfunc ExportNotebookSY(id string) (zipPath string) {\n\t// 加密笔记本必须已解锁才能导出（DEK 在内存才能读 .sy/assets/AV 明文）\n\tif IsEncryptedBox(id) && !IsBoxUnlocked(id) {\n\t\tlogging.LogErrorf(\"export encrypted notebook [%s] failed: locked\", id)\n\t\treturn\n\t}\n\tzipPath = exportBoxSYZip(id)\n\treturn\n}","sourceCodeStart":565,"sourceCodeEnd":601,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/export.go#L565-L601","documentation":"Returned by the withExportReadLockByBlockID guard when the target block belongs to an encrypted notebook whose DEK (data-encryption-key) session is not currently unlocked. The guard is the entry checkpoint for every block-level export (Export2Liandi, ExportPreview, ExportDocx, ExportMarkdownHTML, etc.): it refuses to run fn() so that no ciphertext or empty result is read out. Message text is Conf.Language(314).","triggerScenarios":"Calling any export API whose id resolves to a block in an encrypted notebook while the notebook is locked — i.e. the user has not entered the passphrase, the session expired, or the kernel was restarted (DEK is in-memory only). Also hit if LockBox auto-locked the notebook between the UI action and the API call.","commonSituations":"User scheduled an export or a plugin automated an export against an encrypted notebook without first unlocking it. Kernel restart cleared the in-memory DEK. Auto-lock timeout fired during a long batch.","solutions":["Unlock the encrypted notebook via the UI (enter the passphrase) before exporting.","If scripting, call the unlock API first and confirm it succeeds before issuing the export call.","Increase or disable the auto-lock timeout if exports are long-running and repeatedly get interrupted.","If exporting via a headless/automated flow, ensure the notebook is unlocked in the same kernel session and process (DEK is not persisted)."],"exampleFix":"// before — export called on a still-locked encrypted box\nmodel.ExportDocx(id, savePath, false, false)\n// after — unlock first, then export\nif model.IsEncryptedBox(boxID) && !model.IsBoxUnlocked(boxID) {\n    return errors.New(\"unlock the notebook before exporting\")\n}\nmodel.ExportDocx(id, savePath, false, false)","handlingStrategy":"validation","validationCode":"// Validate unlock state before calling any export that routes through withExportReadLockByBlockID\nbt := getExportBlockTree(id)\nif bt != nil && IsEncryptedBox(bt.BoxID) && !IsBoxUnlocked(bt.BoxID) {\n    return errors.New(Conf.Language(314)) // prompt user to unlock\n}","typeGuard":"// isExportableEncryptedBlock reports whether id is in an encrypted box that is currently unlocked.\nfunc isExportableEncryptedBlock(id string) bool {\n    bt := getExportBlockTree(id)\n    if bt == nil || !IsEncryptedBox(bt.BoxID) {\n        return true // normal boxes are always exportable\n    }\n    return IsBoxUnlocked(bt.BoxID)\n}","tryCatchPattern":"if err := model.ExportDocx(id, savePath, false, false); err != nil {\n    if err.Error() == Conf.Language(314) {\n        // prompt: unlock the encrypted notebook and retry\n    }\n}","preventionTips":["Unlock encrypted notebooks before invoking any export API.","In automated flows, call unlock and verify IsBoxUnlocked before exporting.","Surface the unlock requirement to the user UI before they click export."],"tags":["encryption","export","guard","notebook-lock"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}