siyuan-note/siyuan · error

invalid notebook ID

Error message

invalid notebook ID

What it means

Thrown by UnlockBox (crypto.go:1334) when boxID does not satisfy ast.IsNodeIDPattern. SiYuan notebook IDs are fixed-format node IDs (timestamp-based, alphanumeric); a malformed ID cannot correspond to a real notebook, so unlocking is rejected before any lock is taken or KEK derived. This is an input-validation guard at the public API boundary.

Source

Thrown at kernel/model/crypto.go:1334

				logging.LogWarnf("fix encrypted box conf from backup [%s] failed: %s", boxID, saveErr)
			}
			if needWriteNotebookCryptBackup(boxID, backup) {
				if writeErr := writeNotebookCryptBackup(boxID, backup); writeErr != nil {
					logging.LogWarnf("refresh notebook crypt backup [%s] failed: %s", boxID, writeErr)
				}
			}
			return dek, backup, nil
		}
	}
	return nil, nil, fmt.Errorf("decrypt box [%s] failed: incorrect key or corrupted data", boxID)
}

// UnlockBox 用主密码派生 KEK,解出该笔记本的 DEK 并缓存。KEK 用完即弃,不全局缓存。
// 每次调用都跑一次 Argon2id(约 1 秒),严格满足"每笔记本单独解锁"语义。
func UnlockBox(boxID string, password string, boxEnc *conf.BoxEncryption) (err error) {
	invalidateEncryptedPublishAccessCache()
	if !ast.IsNodeIDPattern(boxID) {
		return errors.New("invalid notebook ID")
	}

	// 全局配置锁先于笔记本生命周期锁获取(设计 §17 锁顺序约定),避免与持子系统锁后回取配置锁的路径死锁。
	// notebookCryptoMu 持锁期间调用的 deriveKEK/conf 修复只申请 Conf.m/cachedDEKsLock,不回取 box 生命周期锁。
	notebookCryptoMu.Lock()
	defer notebookCryptoMu.Unlock()
	releaseTransition := holdEncryptedBoxTransition(boxID)
	defer releaseTransition()
	return unlockBoxHeld(boxID, password, boxEnc)
}

// UnlockAndMountBox 在同一个笔记本转换锁内完成解锁和挂载,挂载失败时回滚本次新建的解锁状态。
func UnlockAndMountBox(boxID, password string, boxEnc *conf.BoxEncryption) (alreadyMount bool, err error) {
	invalidateEncryptedPublishAccessCache()
	if !ast.IsNodeIDPattern(boxID) {
		return false, errors.New("invalid notebook ID")
	}

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Pass a real notebook ID obtained from Conf.Box / ListNotebooks / the notebook list endpoint.
  2. Validate the ID with ast.IsNodeIDPattern before calling UnlockBox.
  3. If calling via HTTP, the API layer's InvalidIDPattern check returns first; fix the client to send the correct ID.

Example fix

// before
model.UnlockBox("my-notebook", password, boxCrypt)

// after
id := box.ID // a valid node ID from Conf.Box / ListNotebooks
model.UnlockBox(id, password, boxCrypt)
Defensive patterns

Strategy: validation

Validate before calling

// Validate the notebook ID before unlocking.
if !ast.IsNodeIDPattern(boxID) {
    return fmt.Errorf("invalid notebook ID: %q", boxID)
}
model.UnlockBox(boxID, password, boxCrypt)

Type guard

func isValidNotebookID(id string) bool { return ast.IsNodeIDPattern(id) }

Try / catch

if err := model.UnlockBox(boxID, password, boxCrypt); err != nil {
    if err.Error() == "invalid notebook ID" {
        respond(c, "provide a valid notebook ID")
        return
    }
    respond(c, err.Error())
}

Prevention

When it happens

Trigger: POST /api/notebook/unlockNotebook with a 'notebook' field that is empty, contains non-ID characters, is the wrong length, or is a path/special value. Note: the API handler also calls util.InvalidIDPattern(notebook) earlier, so reaching this model-layer guard means the caller invoked UnlockBox directly with a bad ID.

Common situations: Internal caller passing a stale, truncated, or hand-constructed ID. Copy/paste error in an ID. Test calling UnlockBox with a placeholder string.

Related errors


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