siyuan-note/siyuan · error

Conf.Language(314)

Error message

Conf.Language(314)

What it means

When copying an asset out of an encrypted notebook, the code takes the box read lock and asks GetDEKIfUnlocked for the notebook's data-encryption key. If the notebook is still locked (user has not supplied the passphrase, so no DEK is cached in memory), the operation fails closed with the localized message for language key 314 ('Please unlock the encrypted notebook first') rather than copying ciphertext that would be unusable or leak file material.

Source

Thrown at kernel/model/crypto.go:2584

	return &crypt, nil
}

// copyAssetDecryptIfEncrypted 把 srcPath 的 asset 复制到 destPath。
// 若 srcPath 在已解锁的加密笔记本下,读密文→解密→写明文到 destPath(导出目录);
// 否则走 filelock.Copy 原路径(字节级复制,密文/明文均可)。
func copyAssetDecryptIfEncrypted(srcPath, destPath string) error {
	if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
		return err
	}

	boxID := ExtractBoxIDFromAssetsPath(srcPath)
	if boxID != "" && IsEncryptedBox(boxID) {
		HoldBoxReadLock(boxID)
		defer ReleaseBoxReadLock(boxID)
		dek, err := GetDEKIfUnlocked(boxID)
		if err != nil {
			// 加密笔记本未解锁:fail-closed,拒绝复制(不复制密文,避免泄漏无效文件)
			return errors.New(Conf.Language(314))
		}
		raw, readErr := filelock.ReadFile(srcPath)
		if readErr != nil {
			return readErr
		}
		diskName := filepath.Base(srcPath)
		plain, decErr := DecryptAsset(boxID, diskName, dek, raw)
		if decErr != nil {
			return errors.New(Conf.Language(316))
		}
		if err := filelock.WriteFile(destPath, plain); err != nil {
			return err
		}
		return nil
	}
	return filelock.Copy(srcPath, destPath)
}

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Unlock the encrypted notebook first (open it in the UI and enter the passphrase, or call the kernel unlock API with the correct password) and retry the copy
  2. For automation, call the unlock endpoint as part of the script before any asset operations
  3. If the notebook cannot be unlocked, the password or key material is wrong — recover from the BoxCrypt backup instead of retrying

Example fix

// before
await fetchPost("/api/filetree/copyAsset", {srcID: assetID, destID: otherBox});
// after
await fetchPost("/api/notebook/unlockEncrypted", {box: encryptedBoxID, password});
await fetchPost("/api/filetree/copyAsset", {srcID: assetID, destID: otherBox});
Defensive patterns

Strategy: try-catch

Validate before calling

// query notebook state first
const nb = await fetchPost("/api/notebook/info", {notebook: boxID});
if (nb.data.box && nb.data.box.encrypted && !unlockedBoxes.has(boxID)) {
    await unlockNotebook(boxID, password);
}

Try / catch

try {
    await copyAsset(srcPath, destPath);
} catch (e) {
    if (String(e.message).includes("unlock the encrypted notebook")) {
        await unlockNotebook(boxID, password);
        return copyAsset(srcPath, destPath);
    }
    throw e;
}

Prevention

When it happens

Trigger: Invoking asset copy/export APIs (e.g. copying an asset to another notebook or exporting) for a box where IsEncryptedBox(boxID) is true while the notebook has not been unlocked with its password in the current session (or was re-locked after timeout).

Common situations: Scripting/API automation against the kernel without performing the unlock step first; kernel restarted, clearing in-memory DEKs, then an asset operation is attempted; multiple users on one workspace where only one has unlocked the notebook.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/6c3a8e14e097a99a. Report an issue: GitHub.