siyuan-note/siyuan · critical

encrypted notebook has no valid key material

Error message

encrypted notebook has no valid key material

What it means

GetBoxEncryption returns this error when conf.json marks the notebook as encrypted (or IsEncryptedBox agrees) but neither conf nor the per-notebook backup contains usable key material (WrappedDEK). It is an explicit integrity error: reporting 'not encrypted' would risk silently writing plaintext, so the kernel fails loudly instead.

Solutions

  1. Restore the notebook's conf.json and per-notebook crypt backup from a sync/backup snapshot taken before the corruption
  2. Check whether a master-password migration is pending - restart SiYuan so recovery can rebuild conf from backup or migration entry
  3. Verify the data/<boxID>/.siyuan/ directory actually contains the key-material files; re-copy the missing one from a healthy workspace copy
  4. Do NOT set Encrypted=false to silence the error - that would expose plaintext writes; if the notebook is genuinely unusable, decrypt-recover its content from backup and re-create it

Example fix

// before: conf says encrypted, key material missing
boxConf.Encrypted = true // BoxCrypt.WrappedDEK empty -> error: encrypted notebook has no valid key material
// after: restore the box's .siyuan conf/backup (with WrappedDEK) from snapshot, or complete pending migration recovery via restart
Defensive patterns

Strategy: try-catch

Validate before calling

// Check key material exists before treating a box as encrypted
confMarked := boxConf != nil && boxConf.Encrypted
hasKeys := confMarked && boxConf.BoxCrypt != nil && len(boxConf.BoxCrypt.WrappedDEK) > 0
if confMarked && !hasKeys {
    // restore conf/backup from snapshot before calling GetBoxEncryption
}

Type guard

func hasKeyMaterial(be *conf.BoxEncryption) bool {
    return be != nil && len(be.WrappedDEK) > 0
}

Try / catch

enc, err := model.GetBoxEncryption(boxID)
if err != nil {
    if err.Error() == "encrypted notebook has no valid key material" {
        // DO NOT fall back to treating the box as plaintext; restore key material from backup/snapshot first
    }
}

Prevention

When it happens

Trigger: Calling GetBoxEncryption for a notebook whose conf.json has Encrypted=true but whose BoxCrypt.WrappedDEK is empty/invalid AND readNotebookCryptBackup returns nil or a backup without WrappedDEK.

Common situations: conf.json manually edited or partially written (interrupted save); a workspace restored where the notebook crypt backup was excluded; sync conflicts keeping Encrypted=true while stripping key material; an encrypted notebook copied into a workspace without its metadata files.

Related errors


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

Appendix: source

Thrown at kernel/model/crypto.go:1951

	// conf 中有完整的 BoxCrypt
	if confMarkedEncrypted && boxConf.BoxCrypt != nil && len(boxConf.BoxCrypt.WrappedDEK) > 0 {
		return boxConf.BoxCrypt, nil
	}

	// fallback 到 backup
	backup, err := readNotebookCryptBackup(boxID)
	if err != nil {
		return nil, err
	}
	if backup != nil && len(backup.WrappedDEK) > 0 {
		markRuntimeEncryptedBox(boxID)
		return backup, nil
	}

	// backup 也不可用
	if confMarkedEncrypted || IsEncryptedBox(boxID) {
		// conf 标记为加密但密钥材料缺失 → 明确错误(而非误报"未加密")
		return nil, errors.New("encrypted notebook has no valid key material")
	}
	return nil, nil // 真正的非加密笔记本
}

// needWriteNotebookCryptBackup 检查是否需要写入/刷新 per-notebook backup。
// backup 不存在、或内容与 crypt 不一致时返回 true。
func needWriteNotebookCryptBackup(boxID string, crypt *conf.BoxEncryption) bool {
	existing, err := readNotebookCryptBackup(boxID)
	if err != nil || existing == nil {
		return true
	}
	return !bytes.Equal(existing.WrappedDEK, crypt.WrappedDEK) ||
		!bytes.Equal(existing.WrapNonce, crypt.WrapNonce) ||
		!bytes.Equal(existing.Metadata, crypt.Metadata) ||
		existing.Spec != crypt.Spec ||
		existing.CreatedAt != crypt.CreatedAt
}

View on GitHub (pinned to 9f775e8a12)