siyuan-note/siyuan · critical

no encrypted key material for box

Error message

no encrypted key material for box

What it means

Thrown by unlockBoxHeld (crypto.go:1375) when the provided boxEnc is nil or its WrappedDEK is empty, meaning there is no encrypted key material to unwrap a DEK from. The function sets the box state to EncryptedBoxStateError and returns, so the notebook is flagged as broken rather than silently treated as unlocked/unencrypted. This usually indicates the notebook's encryption metadata (conf BoxCrypt or the per-notebook backup) is missing or was wiped.

Source

Thrown at kernel/model/crypto.go:1375

	wasUnlocked := IsBoxUnlocked(boxID)
	if err = unlockBoxHeld(boxID, password, boxEnc); err != nil {
		return false, err
	}
	alreadyMount, err = mountBox(boxID)
	if err != nil && !wasUnlocked {
		lockBoxWithPreparationHeld(boxID, nil)
	}
	return alreadyMount, err
}

func unlockBoxHeld(boxID string, password string, boxEnc *conf.BoxEncryption) (err error) {
	if _, busy := boxLock.Load(boxID); busy {
		return errors.New(Conf.language(239))
	}
	if boxEnc == nil || len(boxEnc.WrappedDEK) == 0 {
		setEncryptedBoxState(boxID, EncryptedBoxStateError)
		return errors.New("no encrypted key material for box")
	}
	if IsBoxUnlocked(boxID) {
		if GetEncryptedBoxState(boxID) == EncryptedBoxStateError {
			return errors.New(Conf.Language(316))
		}
		setEncryptedBoxState(boxID, EncryptedBoxStateUnlocked)
		return nil
	}

	setEncryptedBoxState(boxID, EncryptedBoxStateUnlocking)
	// 获取 box 写锁,与 LockBox/unmount0 串行化,防止并发锁/解锁导致 db/DEK 状态不一致
	acquireBoxWriteLock(boxID)
	finalState := EncryptedBoxStateLocked
	defer func() {
		releaseBoxWriteLock(boxID)
		setEncryptedBoxState(boxID, finalState)
	}()

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Restore the notebook's BoxCrypt (WrappedDEK) from its per-notebook key backup or from conf.json backup, then retry.
  2. If the key material is unrecoverable and the data is expendable, remove the notebook and recreate it.
  3. Do not call unlockBoxHeld with a nil boxEnc; fetch it via GetBoxEncryption and handle nil at the call site.

Example fix

// before
model.UnlockBox(boxID, password, nil)

// after
boxCrypt, err := model.GetBoxEncryption(boxID)
if err != nil || boxCrypt == nil || len(boxCrypt.WrappedDEK) == 0 {
    return fmt.Errorf("no key material for notebook %s", boxID)
}
model.UnlockBox(boxID, password, boxCrypt)
Defensive patterns

Strategy: validation

Validate before calling

// Ensure key material exists before unlocking.
boxCrypt, err := model.GetBoxEncryption(boxID)
if err != nil {
    return fmt.Errorf("cannot read encryption metadata: %w", err)
}
if boxCrypt == nil || len(boxCrypt.WrappedDEK) == 0 {
    return fmt.Errorf("no encrypted key material for notebook %s; restore its backup", boxID)
}
model.UnlockBox(boxID, password, boxCrypt)

Type guard

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

Try / catch

if err := model.UnlockBox(boxID, password, boxCrypt); err != nil {
    if err.Error() == "no encrypted key material for box" {
        respond(c, "notebook key material missing; restore the notebook backup or remove the notebook")
        return
    }
    respond(c, err.Error())
}

Prevention

When it happens

Trigger: UnlockBox/UnlockAndMountBox called with a boxEnc that GetBoxEncryption returned as nil or with empty WrappedDEK. Note the HTTP handler checks this earlier (returns 319), so reaching here means a direct Go caller passed a nil/empty boxEnc, or the box's conf and backup both lack WrappedDEK.

Common situations: Notebook conf.json lost its BoxCrypt field (corruption/bad sync) and no per-notebook backup exists. Caller passed nil explicitly. Notebook directory partially deleted, leaving Encrypted=true but no key material.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/ecab7b95d651445b. Report an issue: GitHub.