siyuan-note/siyuan · error
source is not an encrypted asset
Error message
source is not an encrypted asset
What it means
copyDecryptedAsset extracts the notebook (box) ID from the source assets path via model.ExtractBoxIDFromAssetsPath and requires it to refer to an encrypted notebook. If the path does not resolve to an assets path of an encrypted box (empty box ID, or box not marked encrypted), the function refuses to decrypt-copy with this error.
Solutions
- Verify the source path is of the form <workspace>/data/<boxID>/assets/... for an encrypted notebook.
- Check model.IsEncryptedBox(model.ExtractBoxIDFromAssetsPath(src)) before calling the API and use the normal file-read path for unencrypted assets.
- Pass the asset path exactly as returned by kernel asset APIs rather than constructing it manually.
- If the notebook should be encrypted, confirm the box's encryption state/config; a recently migrated notebook may no longer be flagged encrypted.
Example fix
// before
boxID := model.ExtractBoxIDFromAssetsPath(src)
_ = copyDecryptedAsset(src, dest) // errors for plain notebooks
// after
boxID := model.ExtractBoxIDFromAssetsPath(src)
if boxID == "" || !model.IsEncryptedBox(boxID) {
return model.EnsureAssetLocal(src) // plain asset: use normal copy path
}
err := copyDecryptedAsset(src, dest) Defensive patterns
Strategy: validation
Validate before calling
const boxID = extractBoxIDFromAssetsPath(src) if (!boxID || !isEncryptedBox(boxID)) usePlainCopyPath(src) else copyDecryptedAsset(src, dest)
Type guard
function isEncryptedAssetPath(p) {
const m = p.match(/[\\/]data[\\/]([^\\/]+)[\\/]assets[\\/]/)
return !!m && isEncryptedBox(m[1])
} Try / catch
try { await copyDecryptedAsset(src, dest) }
catch (e) { if (e.message === "source is not an encrypted asset") await plainCopy(src, dest) else throw e } Prevention
- Always obtain src from kernel asset APIs instead of string-building paths
- Check box encryption state before choosing the decrypt path
- Keep notebook id/encryption flags in sync after migrations
When it happens
Trigger: Calling the decrypt-copy API with src pointing to an asset in a plain (unencrypted) notebook, or a path that is not a valid assets path (e.g. outside data/<boxID>/assets/, or a notebook ID extracted as empty string).
Common situations: Caller passes a global /assets/ path or a hand-built path not matching data/<boxID>/assets/; the notebook was converted to unencrypted; the asset belongs to a different feature (e.g. temp or emb folder).
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- invalid .sy base name
- refuse to write decrypted asset inside workspace
- accessing assets in encrypted notebook
- Argon2id KeyLength must be 32
- Argon2id Memory too low (minimum 64 MB)
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/590cbef7a47e5087.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/api/file.go:72
// 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")
}
if !model.IsBoxUnlocked(boxID) {
return fmt.Errorf("%s", model.Conf.Language(314))
}
if err := model.EnsureAssetLocal(src); err != nil {
return err
}
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
}View on GitHub (pinned to 9f775e8a12)