siyuan-note/siyuan · error

encrypted notebook is locked, please unlock it first

Error message

encrypted notebook is locked, please unlock it first

What it means

GetDEKIfUnlocked in kernel/model/crypto.go returns the per-notebook data-encryption key (DEK) only when the encrypted notebook has been unlocked in the current session. The kernel throws this error when the notebook is encrypted but no unlocking passphrase has been supplied yet (or the cached DEK was evicted after relock), so no key material can be handed to the caller. It is a deliberate guard to avoid silently operating on an encrypted notebook without its key.

Solutions

  1. Unlock the notebook first (call the encrypted-notebook unlock API / UI prompt with the passphrase) before retrying the operation
  2. Check isBoxUnlockedForAccess equivalent via GetDEKIfUnlocked before scheduling background operations on encrypted notebooks
  3. Re-open/re-cache the DEK by unlocking; if the DEK was evicted while 'unlocked', unlock again to repopulate cachedDEKs

Example fix

// before
dek, err := model.GetDEKIfUnlocked(boxID)
if err != nil { return err }
// after
if !model.IsEncryptedBox(boxID) {
    // non-encrypted path
} else if dek, err = model.GetDEKIfUnlocked(boxID); err != nil {
    return fmt.Errorf("notebook %s must be unlocked: %w", boxID, err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

if model.IsEncryptedBox(boxID) {
    if _, err := model.GetDEKIfUnlocked(boxID); err != nil {
        return promptUserToUnlock(boxID)
    }
}

Type guard

func isNotebookOperable(boxID string) bool {
    return !model.IsEncryptedBox(boxID) || func() bool {
        _, err := model.GetDEKIfUnlocked(boxID)
        return err == nil
    }()
}

Try / catch

dek, err := model.GetDEKIfUnlocked(boxID)
if err != nil {
    if strings.Contains(err.Error(), "locked, please unlock") {
        return unlockAndRetry(boxID, op)
    }
    return err
}

Prevention

When it happens

Trigger: Calling GetDEKIfUnlocked(boxID) (or any caller such as sync/upsertIndexes, asset encryption, history, export paths) on a notebook where IsEncryptedBox(boxID) is true while isBoxUnlockedForAccess(boxID) is false, or where boxID is missing from cachedDEKs after the notebook was re-locked.

Common situations: Kernel restarted and the encrypted notebook was never unlocked via the unlock API before sync, search, export, or asset upload; a plugin/script drives the HTTP API without performing the unlock step; a background job races a user re-locking the notebook.

Related errors


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

Appendix: source

Thrown at kernel/model/crypto.go:2123

func IsEncryptedAssetPath(absPath string) bool {
	boxID := ExtractBoxIDFromAssetsPath(absPath)
	return boxID != "" && IsEncryptedBox(boxID)
}

// GetDEKIfUnlocked 返回已解锁加密笔记本的 DEK(副本)。
// 非加密笔记本返回 (nil, nil)——filesys 据此原样读写,对普通笔记本透明。
// 加密但未解锁(DEK 不在内存)返回 (nil, error)——filesys 的加解密函数遇 error 后拒绝读写,
// 避免加密笔记本在未解锁状态下静默以明文落盘(深度防御,见 issue #18034)。
func GetDEKIfUnlocked(boxID string) ([]byte, error) {
	if boxID != "" && !ast.IsNodeIDPattern(boxID) {
		return nil, errors.New("invalid notebook ID")
	}
	if !IsEncryptedBox(boxID) {
		return nil, nil
	}
	repairEncryptedBoxStateFromDEK(boxID)
	if !isBoxUnlockedForAccess(boxID) {
		return nil, errors.New("encrypted notebook is locked, please unlock it first")
	}
	cachedDEKsLock.RLock()
	defer cachedDEKsLock.RUnlock()
	dek, ok := cachedDEKs[boxID]
	if !ok {
		return nil, errors.New("encrypted notebook is locked, please unlock it first")
	}
	ret := make([]byte, len(dek))
	copy(ret, dek)
	return ret, nil
}

// HoldBoxReadLock 获取 box 读锁,防止 LockBox 在持锁期间清除缓存/临时文件。
// 调用方完成解密输出后必须调 ReleaseBoxReadLock。
func HoldBoxReadLock(boxID string) {
	if !IsEncryptedBox(boxID) {
		acquireBoxReadLock(boxID)
		return

View on GitHub (pinned to 9f775e8a12)