{"record":{"id":"2426131bd22943b7","repo":"siyuan-note/siyuan","slug":"encrypted-notebook-asset-history-is-plaintext-s","errorCode":null,"errorMessage":"encrypted notebook asset history is plaintext [%s]","messagePattern":"encrypted notebook asset history is plaintext \\[(.+?)\\]","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/history.go","lineNumber":567,"sourceCode":"\t}\n\n\tfrom := historyPath\n\t// 从路径提取 boxID 判断是否加密笔记本的资源\n\trelPath := strings.TrimPrefix(filepath.ToSlash(historyPath), filepath.ToSlash(util.HistoryDir))\n\trelPath = strings.TrimPrefix(relPath, \"/\")\n\tpathParts := strings.SplitN(relPath, \"/\", 3)\n\tdata, readErr := filelock.ReadFile(from)\n\tif readErr != nil {\n\t\treturn readErr\n\t}\n\tencrypted := bytes.HasPrefix(data, encryptedAssetMagic)\n\tif encrypted && (len(pathParts) < 3 || !ast.IsNodeIDPattern(pathParts[1]) || !IsEncryptedBox(pathParts[1])) {\n\t\treturn errors.New(\"encrypted asset history is missing valid notebook context\")\n\t}\n\tto := filepath.Join(util.DataDir, \"assets\", filepath.Base(historyPath))\n\tif len(pathParts) >= 2 && IsEncryptedBox(pathParts[1]) {\n\t\tif !encrypted {\n\t\t\treturn fmt.Errorf(\"encrypted notebook asset history is plaintext [%s]\", pathParts[1])\n\t\t}\n\t\t// 加密笔记本的资源回滚到笔记本级 assets 目录\n\t\tto = filepath.Join(util.DataDir, pathParts[1], \"assets\", filepath.Base(historyPath))\n\t\tif err = os.MkdirAll(filepath.Dir(to), 0755); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\tif err = filelock.CopyNewtimes(from, to); err != nil {\n\t\tlogging.LogErrorf(\"copy file [%s] to [%s] failed: %s\", from, to, err)\n\t\treturn\n\t}\n\tIncSync()\n\tutil.PushMsg(Conf.Language(102), 3000)\n\treturn nil\n}\n\n// validateHistoryPath 校验历史路径是否位于工作区内且属于历史目录。","sourceCodeStart":549,"sourceCodeEnd":585,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/history.go#L549-L585","documentation":"When rolling back an asset history that lives under an encrypted notebook's history directory, SiYuan requires the stored snapshot to actually be encrypted. The history file content was read and found to lack encryptedAssetMagic (i.e. plaintext) even though the path points into an encrypted notebook, so the rollback is rejected to avoid silently restoring unprotected data into an encrypted notebook.","triggerScenarios":"Calling rollbackAssetsHistory with a historyPath under an encrypted notebook (IsEncryptedBox(pathParts[1]) is true) whose file on disk is plaintext — e.g. the snapshot was written before encryption or was tampered/decrypted externally.","commonSituations":"Restoring old history snapshots taken before the encrypted-notebook feature; a failed/interrupted encryption migration left plaintext snapshots; manual editing or copying of files inside data/history/; version downgrade/upgrade mixing encrypted and unencrypted history.","solutions":["Re-create the history snapshot so the asset is re-encrypted at write time, then roll back again","Remove the stale plaintext history entry and re-generate history for that notebook","Verify with the notebook encryption migration tooling that the notebook's history was re-encrypted after enabling encryption","Do not hand-copy plaintext assets into data/history/ of encrypted notebooks"],"exampleFix":"// before: plaintext snapshot in encrypted notebook history\n//   data/history/20240101120000-update/<encryptedBoxID>/assets/img.png\n// after: re-generate the snapshot as ciphertext\n//   data/history/20240101120000-update/<encryptedBoxID>/assets/img.png.enc\nrollbackAssetsHistory(\"data/history/20240101120000-update/<encryptedBoxID>/assets/img.png.enc\")","handlingStrategy":"validation","validationCode":"// Pre-check snapshot content is ciphertext before rollback (read first bytes of the history file)\nconst head = await readHistoryFileHead(historyPath);\nif (isEncryptedNotebook(boxID) && !hasEncryptedMagic(head)) {\n  throw new Error(\"plaintext snapshot inside encrypted notebook history\");\n}","typeGuard":"null","tryCatchPattern":"try { await rollbackAssetsHistory(p); } catch (e) { if (String(e.msg).startsWith(\"encrypted notebook asset history is plaintext\")) { await regenerateSnapshotEncrypted(p); await rollbackAssetsHistory(p); } else { throw e; } }","preventionTips":["Do not hand-copy or decrypt files inside data/history of encrypted notebooks","Re-run encryption migration for notebooks upgraded from plaintext","Keep backups of history intact rather than editing snapshots in place"],"tags":["history","encryption","assets","data-integrity"],"backgroundTag":"checksum-mismatch","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"}