siyuan-note/siyuan · error

encrypted notebook attribute view history is plaintext [%s]

Error message

encrypted notebook attribute view history is plaintext [%s]

What it means

When rolling back an AV history for an encrypted notebook (path contains an encrypted boxID), the code requires the history snapshot file itself to be ciphertext. If the stored history JSON is plaintext while the notebook is encrypted, the snapshot violates the encrypted-notebook storage contract and restoring it would write unencrypted attribute-view definitions into an encrypted notebook. The error names the offending boxID so the bad snapshot can be located.

Source

Thrown at kernel/model/history.go:697

	}

	from := historyPath
	// 从路径提取 boxID 判断是否加密笔记本的 AV
	relPath := strings.TrimPrefix(filepath.ToSlash(historyPath), filepath.ToSlash(util.HistoryDir))
	relPath = strings.TrimPrefix(relPath, "/")
	pathParts := strings.SplitN(relPath, "/", 3)
	data, readErr := filelock.ReadFile(from)
	if readErr != nil {
		return readErr
	}
	ciphertext := util.IsCiphertext(data)
	if ciphertext && (len(pathParts) < 3 || !ast.IsNodeIDPattern(pathParts[1]) || !IsEncryptedBox(pathParts[1])) {
		return errors.New("encrypted attribute view history is missing valid notebook context")
	}
	to := filepath.Join(util.DataDir, "storage", "av", filepath.Base(historyPath))
	if len(pathParts) >= 2 && IsEncryptedBox(pathParts[1]) {
		if !ciphertext {
			return fmt.Errorf("encrypted notebook attribute view history is plaintext [%s]", pathParts[1])
		}
		// 加密笔记本的 AV 定义回滚到笔记本级目录
		to = filepath.Join(util.DataDir, pathParts[1], "storage", "av", filepath.Base(historyPath))
		if err = os.MkdirAll(filepath.Dir(to), 0755); err != nil {
			return
		}
	}

	if err = filelock.CopyNewtimes(from, to); err != nil {
		logging.LogErrorf("copy file [%s] to [%s] failed: %s", from, to, err)
		return
	}
	avID := strings.TrimSuffix(filepath.Base(historyPath), ".json")
	cache.RemoveAVData(avID)
	avBoxID := ""
	if len(pathParts) >= 2 && IsEncryptedBox(pathParts[1]) {
		avBoxID = pathParts[1]
	}

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Replace the plaintext history snapshot at that path with a properly encrypted one from a valid backup
  2. Do not roll back this snapshot; manually recreate the attribute view inside the encrypted notebook so it is written encrypted
  3. Verify with util.IsCiphertext (or open the file and confirm it is ciphertext) before attempting rollback again
  4. Check kernel version: if an older build produced the plaintext history, regenerate history after upgrading
Defensive patterns

Strategy: validation

Validate before calling

const data = await readFile(historyPath);
const isCiphertext = (buf) => !Buffer.from(buf.slice(0, 64)).toString("utf8").trimStart().startsWith("{");
const notebookIsEncrypted = isEncryptedBox(boxIDFromPath);
if (notebookIsEncrypted && !isCiphertext(data)) {
  throw new Error("plaintext AV history for encrypted notebook " + boxIDFromPath);
}

Prevention

When it happens

Trigger: Calling RollbackAttributeViewHistory with a history path under an encrypted notebook ID (pathParts[1] passes IsEncryptedBox) but the history file read from disk fails util.IsCiphertext — e.g. the .json was saved in plaintext, produced by old tooling, or partially re-written by a sync/restore process.

Common situations: Restoring history snapshots from a backup made before encrypted-notebook AV support; third-party scripts that copy storage/av JSONs around without encrypting them; a mixed-version workspace where an older kernel wrote plaintext AV history for an encrypted notebook.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/46936d57d65d3ccf. Report an issue: GitHub.