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
- Unlock the notebook first (call the encrypted-notebook unlock API / UI prompt with the passphrase) before retrying the operation
- Check isBoxUnlockedForAccess equivalent via GetDEKIfUnlocked before scheduling background operations on encrypted notebooks
- 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
- Always run the unlock flow before scheduling sync/export/search jobs on encrypted notebooks
- After kernel restart, treat every encrypted notebook as locked until explicitly unlocked
- Retry operations once after a successful unlock; do not loop retries while the notebook stays locked
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
- accessing assets in encrypted notebook
- Conf.Language(316) + " [box=" + id + "]"
- decrypt box [ ] failed: incorrect key or corrupted data
- encrypted asset metadata is too large
- encrypted attribute view snapshot has no matching notebook
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)
returnView on GitHub (pinned to 9f775e8a12)