siyuan-note/siyuan · critical

no encrypted key material for box

Error message

no encrypted key material for box [%s]

What it means

decryptBoxCrypt returns this error when the notebook's BoxEncryption record cannot be loaded, is nil, or has an empty WrappedDEK, meaning there is no encrypted key material to unwrap the notebook's DEK. Without a WrappedDEK the notebook cannot be decrypted with any password. It guards against attempting decryption on a notebook whose crypto configuration is missing or corrupt.

Solutions

  1. Check the notebook's conf.json contains an Encrypted flag and BoxCrypt.WrappedDEK; restore it from a backup if missing
  2. Verify the notebook crypto backup file exists and holds a WrappedDEK for this box so the auto-repair path can work
  3. Confirm you are unlocking a notebook that was actually encrypted; non-encrypted notebooks have no key material
  4. Re-sync the workspace from a peer that still has intact key material

Example fix

// before
model.UnlockBox(boxID, password, boxEnc) // boxEnc loaded from stale/partial conf
// after
if boxEnc == nil || len(boxEnc.WrappedDEK) == 0 {
    return fmt.Errorf("box %s has no key material; restore conf.json or decrypt is impossible", boxID)
}
model.UnlockBox(boxID, password, boxEnc)
Defensive patterns

Strategy: validation

Validate before calling

boxEnc, err := model.GetBoxEncryption(boxID)
if err != nil || boxEnc == nil || len(boxEnc.WrappedDEK) == 0 {
    return fmt.Errorf("box %s has no encrypted key material; refusing unlock", boxID)
}

Type guard

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

Prevention

When it happens

Trigger: decryptBoxCrypt(boxID, kek) is called by unlockBoxHeld (via UnlockBox/UnlockAndMountBox), ChangeMasterPassword, or tests when GetBoxEncryption fails, or conf.json lacks BoxCrypt.WrappedDEK for the given notebook ID.

Common situations: Encrypted notebook's conf.json was edited/truncated or lost its BoxCrypt section; notebook was re-created or synced without its crypto config; callers passing a boxID of a non-encrypted notebook; backup file missing while conf is damaged.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


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

Appendix: source

Thrown at kernel/model/crypto.go:1324

			return nil, errMasterPasswordMigrationPending
		}
		if err = saveNotebookCryptoBackup(kek); err != nil {
			zeroAndClear(kek)
			return nil, fmt.Errorf("%w: %v", errMasterPasswordMigrationPending, err)
		}
		removeMasterPasswordMigration()
	}
	return kek, nil
}

// decryptBoxCrypt 用 KEK 解密 box 的 WrappedDEK。优先使用 GetBoxEncryption 的结果(conf → backup fallback),
// 若解密失败则尝试 backup 中不同的 WrappedDEK。
// 返回解密后的 DEK 和实际使用的 BoxCrypt(可能来自 backup)。
// 若 backup 被使用会自动修复 conf.json 和刷新 backup。
func decryptBoxCrypt(boxID string, kek []byte) (dek []byte, boxCrypt *conf.BoxEncryption, err error) {
	boxCrypt, err = GetBoxEncryption(boxID)
	if err != nil || boxCrypt == nil || len(boxCrypt.WrappedDEK) == 0 {
		return nil, nil, fmt.Errorf("no encrypted key material for box [%s]", boxID)
	}

	nc := currentNotebookCrypto()
	dek, err = decryptWrappedDEKWithHistory(boxID, boxCrypt, kek, nc)
	if err == nil {
		return dek, boxCrypt, nil
	}

	// 主 BoxCrypt 无法解密:尝试 backup 中不同的 WrappedDEK
	backup, bErr := readNotebookCryptBackup(boxID)
	if bErr == nil && backup != nil && len(backup.WrappedDEK) > 0 &&
		!bytes.Equal(backup.WrappedDEK, boxCrypt.WrappedDEK) {
		dek, err = decryptWrappedDEKWithHistory(boxID, backup, kek, nc)
		if err == nil {
			// backup 解密成功:修复 conf + 刷新 backup
			box := &Box{ID: boxID}
			boxConf := box.GetConf()
			boxConf.Encrypted = true

View on GitHub (pinned to 9f775e8a12)