siyuan-note/siyuan · error

Access to encrypted notebook data is not supported via this…

Error message

Access to encrypted notebook data is not supported via this API

What it means

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.

Solutions

  1. Restrict the requested path to non-encrypted notebooks; check the target box's encryption state first.
  2. Use the encrypted-notebook-aware APIs (with unlock flow, e.g. copyDecryptedAsset) for assets in encrypted notebooks.
  3. Filter the file list so no entry resolves under an encrypted box's assets directory before calling the API.
  4. Catch message 321 in the UI and explain that encrypted notebook files need the unlock flow.

Example fix

// before
files := [{"path": "/notebook-enc/assets/a.png"}]
await fetchPost("/api/file/copyFiles", {files})

// after
if rejectEncryptedBoxPath(assetPath) {
    await copyDecryptedAsset(srcPath, dest) // unlock-aware path
} else {
    await fetchPost("/api/file/copyFiles", {files})
}
Defensive patterns

Strategy: validation

Validate before calling

if (files.some(f => rejectEncryptedBoxPath(join(dataDir, f.path)))) throw new Error("use unlock-aware API for encrypted notebooks")

Type guard

function isEncryptedBoxAsset(p, encryptedBoxIDs) {
  const rel = require("path").relative(dataDir, p)
  const box = rel.split(require("path").sep)[0]
  return encryptedBoxIDs.includes(box) && rel.split(require("path").sep)[1] === "assets"
}

Try / catch

try { await fetchPost("/api/file/copyFiles", {files}) }
catch (e) { if (e.msg.includes("encrypted")) notify("Encrypted notebook data needs the unlock flow") else throw e }

Prevention

When it happens

Trigger: Calling globalCopyFiles / file download APIs whose absPath equals or is a parent of an asset path inside an encrypted notebook (data/<encryptedBoxID>/assets/...).

Common situations: 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.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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

Appendix: source

Thrown at kernel/api/file.go:128

})

// prepareFileAssets 在原始文件 API 完成权限校验后补齐目录或文件的资源内容。
func prepareFileAssets(absPath string) error {
	absPath = filepath.Clean(absPath)
	dataPath := filepath.Clean(util.DataDir)
	if gulu.File.IsSubPath(absPath, dataPath) {
		absPath = dataPath
	} else if absPath != dataPath && !gulu.File.IsSubPath(dataPath, absPath) {
		return nil
	}
	files, err := model.DeferredSyncAssets()
	if err != nil {
		return err
	}
	for _, file := range files {
		assetPath := filepath.Join(util.DataDir, filepath.FromSlash(strings.TrimPrefix(file.Path, "/")))
		if (absPath == assetPath || gulu.File.IsSubPath(absPath, assetPath)) && rejectEncryptedBoxPath(assetPath) {
			return fmt.Errorf("%s", model.Conf.Language(321))
		}
	}
	return model.EnsureAssetPrefixLocal(absPath)
}

var globalCopyFiles = contractHandler(apicontract.GlobalCopyFiles, func(c *gin.Context, request apicontract.CopyFilesRequest) apicontract.Response[apicontract.Null] {
	ret := gulu.Ret.NewResult()
	var changedPaths []string
	defer func() {
		model.IncSyncIfNeeded(changedPaths...)
	}()

	srcs, destDirArg := request.Srcs, request.DestDir
	for i, src := range srcs {
		if !filepath.IsAbs(src) {
			logging.LogErrorf("global copy files src [%s] is not an absolute path", src)
			ret.Code = -1
			ret.Msg = "Field [srcs]: each path must be absolute"

View on GitHub (pinned to 9f775e8a12)