{"record":{"id":"d63b72ddc552cc45","repo":"siyuan-note/siyuan","slug":"access-to-encrypted-notebook-data-is-not-supported-via-this","errorCode":null,"errorMessage":"Access to encrypted notebook data is not supported via this API","messagePattern":"Access to encrypted notebook data is not supported via this API","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/api/file.go","lineNumber":128,"sourceCode":"})\n\n// prepareFileAssets 在原始文件 API 完成权限校验后补齐目录或文件的资源内容。\nfunc prepareFileAssets(absPath string) error {\n\tabsPath = filepath.Clean(absPath)\n\tdataPath := filepath.Clean(util.DataDir)\n\tif gulu.File.IsSubPath(absPath, dataPath) {\n\t\tabsPath = dataPath\n\t} else if absPath != dataPath && !gulu.File.IsSubPath(dataPath, absPath) {\n\t\treturn nil\n\t}\n\tfiles, err := model.DeferredSyncAssets()\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, file := range files {\n\t\tassetPath := filepath.Join(util.DataDir, filepath.FromSlash(strings.TrimPrefix(file.Path, \"/\")))\n\t\tif (absPath == assetPath || gulu.File.IsSubPath(absPath, assetPath)) && rejectEncryptedBoxPath(assetPath) {\n\t\t\treturn fmt.Errorf(\"%s\", model.Conf.Language(321))\n\t\t}\n\t}\n\treturn model.EnsureAssetPrefixLocal(absPath)\n}\n\nvar globalCopyFiles = contractHandler(apicontract.GlobalCopyFiles, func(c *gin.Context, request apicontract.CopyFilesRequest) apicontract.Response[apicontract.Null] {\n\tret := gulu.Ret.NewResult()\n\tvar changedPaths []string\n\tdefer func() {\n\t\tmodel.IncSyncIfNeeded(changedPaths...)\n\t}()\n\n\tsrcs, destDirArg := request.Srcs, request.DestDir\n\tfor i, src := range srcs {\n\t\tif !filepath.IsAbs(src) {\n\t\t\tlogging.LogErrorf(\"global copy files src [%s] is not an absolute path\", src)\n\t\t\tret.Code = -1\n\t\t\tret.Msg = \"Field [srcs]: each path must be absolute\"","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/api/file.go#L110-L146","documentation":"prepareFileAssets validates paths requested through the file API and calls rejectEncryptedBoxPath for each matched asset path. If the requested file or a sub-path belongs to an encrypted notebook's data, the API refuses access with the localized message 321: encrypted notebook data must not be read or written through generic file APIs. This preserves the encryption boundary: plaintext access only through unlock-aware code paths.","triggerScenarios":"Calling globalCopyFiles / file download APIs whose absPath equals or is a parent of an asset path inside an encrypted notebook (data/<encryptedBoxID>/assets/...).","commonSituations":"Scripts bulk-copy files from data/ without knowing which notebooks are encrypted; sync/backup tools targeting the whole data directory; plugins using the generic file API on an encrypted notebook's assets.","solutions":["Restrict the requested path to non-encrypted notebooks; check the target box's encryption state first.","Use the encrypted-notebook-aware APIs (with unlock flow, e.g. copyDecryptedAsset) for assets in encrypted notebooks.","Filter the file list so no entry resolves under an encrypted box's assets directory before calling the API.","Catch message 321 in the UI and explain that encrypted notebook files need the unlock flow."],"exampleFix":"// before\nfiles := [{\"path\": \"/notebook-enc/assets/a.png\"}]\nawait fetchPost(\"/api/file/copyFiles\", {files})\n\n// after\nif rejectEncryptedBoxPath(assetPath) {\n    await copyDecryptedAsset(srcPath, dest) // unlock-aware path\n} else {\n    await fetchPost(\"/api/file/copyFiles\", {files})\n}","handlingStrategy":"validation","validationCode":"if (files.some(f => rejectEncryptedBoxPath(join(dataDir, f.path)))) throw new Error(\"use unlock-aware API for encrypted notebooks\")","typeGuard":"function isEncryptedBoxAsset(p, encryptedBoxIDs) {\n  const rel = require(\"path\").relative(dataDir, p)\n  const box = rel.split(require(\"path\").sep)[0]\n  return encryptedBoxIDs.includes(box) && rel.split(require(\"path\").sep)[1] === \"assets\"\n}","tryCatchPattern":"try { await fetchPost(\"/api/file/copyFiles\", {files}) }\ncatch (e) { if (e.msg.includes(\"encrypted\")) notify(\"Encrypted notebook data needs the unlock flow\") else throw e }","preventionTips":["Filter file lists against encrypted box ids before generic file APIs","Route encrypted-notebook assets through dedicated unlock-aware endpoints","Keep an up-to-date set of encrypted box ids client-side"],"tags":["encryption","api","security","access-control"],"backgroundTag":"permission-denied","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}