{"record":{"id":"5664629b01f1e3f8","repo":"siyuan-note/siyuan","slug":"s","errorCode":null,"errorMessage":"%s","messagePattern":"%s","errorType":"http","errorClass":null,"httpStatus":200,"severity":"warning","filePath":"kernel/api/file.go","lineNumber":63,"sourceCode":"// 对加密笔记本的任何文件读写都应拒绝——合法读写走专用 API（upload/getBlockKramdown 等，已加密感知），\n// 避免密文泄漏给插件或明文破坏加密格式。\n// 防止 symlink 绕过：找到最长已存在的父路径，解析 symlink 后拼回剩余路径，再检查是否落入加密 box。\nfunc rejectEncryptedBoxPath(absPath string) bool {\n\treturn model.EncryptedRawPathBoxID(absPath) != \"\"\n}\n\n// copyDecryptedAsset 将加密 asset 解密后复制到目标路径（dest 必须在工作区外）。\nfunc copyDecryptedAsset(src, dest string) error {\n\t// 安全守卫：dest 必须在工作区外，防止解密后的明文落入工作区普通目录\n\tif gulu.File.IsSubPath(util.WorkspaceDir, dest) {\n\t\treturn fmt.Errorf(\"refuse to write decrypted asset inside workspace\")\n\t}\n\tboxID := model.ExtractBoxIDFromAssetsPath(src)\n\tif boxID == \"\" || !model.IsEncryptedBox(boxID) {\n\t\treturn fmt.Errorf(\"source is not an encrypted asset\")\n\t}\n\tif !model.IsBoxUnlocked(boxID) {\n\t\treturn fmt.Errorf(\"%s\", model.Conf.Language(314))\n\t}\n\tif err := model.EnsureAssetLocal(src); err != nil {\n\t\treturn err\n\t}\n\tmodel.HoldBoxReadLock(boxID)\n\tdefer model.ReleaseBoxReadLock(boxID)\n\tdek, dekErr := model.GetDEKIfUnlocked(boxID)\n\tif dekErr != nil {\n\t\treturn dekErr\n\t}\n\tdiskName := filepath.Base(src)\n\tdata, readErr := os.ReadFile(src)\n\tif readErr != nil {\n\t\treturn readErr\n\t}\n\tplain, decErr := model.DecryptAsset(boxID, diskName, dek, data)\n\tif decErr != nil {\n\t\treturn decErr","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/api/file.go#L45-L81","documentation":"The error message is the localized string from model.Conf.Language(314), returned when the encrypted notebook owning the asset is locked: model.IsBoxUnlocked(boxID) is false. The kernel refuses to decrypt and copy an asset from a notebook the user has not unlocked, since the plaintext would be derivable without authorization.","triggerScenarios":"Calling copyFile on an asset of an encrypted notebook whose access key has not been provided (or whose unlock session expired) before the copy is requested.","commonSituations":"App restart clears in-memory unlock state; user copied the asset link before locking the box; automated scripts operate on locked notebooks without unlocking first; wrong access key entered earlier left the box locked.","solutions":["Unlock the notebook first via the unlock API/UI with the correct access key, then retry the copy.","Surface the localized message (Language(314)) to the user and prompt for the access key.","Re-unlock after kernel restart since unlock state is not persisted."],"exampleFix":"// before\ncopyFile(src, dest) // box still locked\n\n// after\nunlockBox(boxID, accessKey) // user supplies key\ncopyFile(src, dest)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await copyFile(src, dest) } catch (e) { if (isBoxLockedMessage(e)) { await promptUnlockBox(boxID); await copyFile(src, dest) } else { throw e } }","preventionTips":["Unlock encrypted notebooks before scripted asset operations","Re-unlock after kernel restart; unlock state is not persistent","Surface the localized locked-box message to prompt for the access key"],"tags":["encryption","authentication","locked-notebook"],"backgroundTag":"authentication-required","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}