siyuan-note/siyuan · error

source is not an encrypted asset

Error message

source is not an encrypted asset

What it means

Returned by copyDecryptedAsset (file.go:60) when the source path is not recognized as belonging to an encrypted asset. Either ExtractBoxIDFromAssetsPath(src) returned empty (the path is not under a notebook's assets/ folder), or the extracted box is not an encrypted notebook (IsEncryptedBox is false). The decryption flow only operates on genuine encrypted-box assets, so any other source is rejected before acquiring the box read lock.

Source

Thrown at kernel/api/file.go:60

// rejectEncryptedBoxPath 检查 absPath 是否落在加密笔记本目录下(含 symlink 绕过),是则返回 true。
// 原始文件 API(getFile/putFile/copyFile/renameFile/removeFile)是绕过加密层的逃生口,
// 对加密笔记本的任何文件读写都应拒绝——合法读写走专用 API(upload/getBlockKramdown 等,已加密感知),
// 避免密文泄漏给插件或明文破坏加密格式。
// 防止 symlink 绕过:找到最长已存在的父路径,解析 symlink 后拼回剩余路径,再检查是否落入加密 box。
func rejectEncryptedBoxPath(absPath string) bool {
	return model.EncryptedRawPathBoxID(absPath) != ""
}

// copyDecryptedAsset 将加密 asset 解密后复制到目标路径(dest 必须在工作区外)。
func copyDecryptedAsset(src, dest string) error {
	// 安全守卫:dest 必须在工作区外,防止解密后的明文落入工作区普通目录
	if gulu.File.IsSubPath(util.WorkspaceDir, dest) {
		return fmt.Errorf("refuse to write decrypted asset inside workspace")
	}
	boxID := model.ExtractBoxIDFromAssetsPath(src)
	if boxID == "" || !model.IsEncryptedBox(boxID) {
		return fmt.Errorf("source is not an encrypted asset")
	}
	model.HoldBoxReadLock(boxID)
	defer model.ReleaseBoxReadLock(boxID)
	dek, dekErr := model.GetDEKIfUnlocked(boxID)
	if dekErr != nil {
		return dekErr
	}
	diskName := filepath.Base(src)
	data, readErr := os.ReadFile(src)
	if readErr != nil {
		return readErr
	}
	plain, decErr := model.DecryptAsset(boxID, diskName, dek, data)
	if decErr != nil {
		return decErr
	}
	if writeErr := os.WriteFile(dest, plain, 0644); writeErr != nil {
		return writeErr

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Confirm the source actually lives under an encrypted notebook's assets directory (check IsEncryptedBox on the parsed box ID).
  2. If the asset is not encrypted, use a plain file copy instead of copyDecryptedAsset.
  3. Ensure the path is in the canonical absolute assets form expected by ExtractBoxIDFromAssetsPath (model/crypto.go:2137).
Defensive patterns

Strategy: validation

Validate before calling

boxID := model.ExtractBoxIDFromAssetsPath(src)
if boxID == "" || !model.IsEncryptedBox(boxID) {
  // not an encrypted asset: use plain copy instead of copyDecryptedAsset
}

Prevention

When it happens

Trigger: Calling copyDecryptedAsset with a `src` that points to a regular (non-encrypted) notebook asset, a file outside any assets/ folder, or a path whose box ID cannot be parsed by ExtractBoxIDFromAssetsPath. The check at file.go:59 fails the IsEncryptedBox/boxID guards.

Common situations: Passing a plaintext asset path to the decryption helper (the asset isn't encrypted, so no decryption is needed). Path format mismatch (the assets-path parser expects the standard data/<box>/assets/<diskname> layout). Notebook that was formerly encrypted but is no longer, or vice versa. Bug in a caller that didn't distinguish encrypted from plain assets.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/590cbef7a47e5087. Report an issue: GitHub.