{"record":{"id":"45a63e525e292715","repo":"siyuan-note/siyuan","slug":"please-unlock-the-encrypted-notebook-first","errorCode":null,"errorMessage":"Please unlock the encrypted notebook first","messagePattern":"Please unlock the encrypted notebook first","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/crypto.go","lineNumber":2513,"sourceCode":"\treturn &crypt, nil\n}\n\n// copyAssetDecryptIfEncrypted 把 srcPath 的 asset 复制到 destPath。\n// 若 srcPath 在已解锁的加密笔记本下，读密文→解密→写明文到 destPath（导出目录）；\n// 否则走 filelock.Copy 原路径（字节级复制，密文/明文均可）。\nfunc copyAssetDecryptIfEncrypted(srcPath, destPath string) error {\n\tif err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {\n\t\treturn err\n\t}\n\n\tboxID := ExtractBoxIDFromAssetsPath(srcPath)\n\tif boxID != \"\" && IsEncryptedBox(boxID) {\n\t\tHoldBoxReadLock(boxID)\n\t\tdefer ReleaseBoxReadLock(boxID)\n\t\tdek, err := GetDEKIfUnlocked(boxID)\n\t\tif err != nil {\n\t\t\t// 加密笔记本未解锁：fail-closed，拒绝复制（不复制密文，避免泄漏无效文件）\n\t\t\treturn errors.New(Conf.Language(314))\n\t\t}\n\t\traw, readErr := filelock.ReadFile(srcPath)\n\t\tif readErr != nil {\n\t\t\treturn readErr\n\t\t}\n\t\tdiskName := filepath.Base(srcPath)\n\t\tplain, decErr := DecryptAsset(boxID, diskName, dek, raw)\n\t\tif decErr != nil {\n\t\t\treturn errors.New(Conf.Language(316))\n\t\t}\n\t\tif err := filelock.WriteFile(destPath, plain); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn filelock.Copy(srcPath, destPath)\n}\n","sourceCodeStart":2495,"sourceCodeEnd":2531,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/crypto.go#L2495-L2531","documentation":"Returned by copyAssetDecryptIfEncrypted when GetDEKIfUnlocked fails for the source box — the encrypted notebook is locked. The function is fail-closed: it refuses to copy rather than copying ciphertext (which would produce an unusable file at the destination). The message comes from Conf.Language(314): 'Please unlock the encrypted notebook first'.","triggerScenarios":"An asset copy/export operation (e.g., export to temp, publish, or asset serving) hits copyAssetDecryptIfEncrypted while the source notebook's DEK is not in memory. Triggered during export, thumbnail generation, or asset serving for a locked encrypted notebook.","commonSituations":"User attempts to export or preview an asset from an encrypted notebook that auto-locked. Publish/sync background job runs after the auto-lock timer fired. Fresh app start where the notebook wasn't unlocked.","solutions":["Unlock the encrypted notebook (UnlockBox with the user's password) and retry the copy/export.","Surface a clear UI prompt telling the user which notebook needs unlocking, rather than a generic copy failure.","For background jobs, check isBoxUnlockedForAccess(boxID) before scheduling asset operations and defer or queue the job if locked."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Check unlock state before triggering asset copy\nif model.IsEncryptedBox(boxID) && !model.IsBoxUnlocked(boxID) {\n    return fmt.Errorf(\"please unlock notebook %s first\", boxID)\n}","typeGuard":null,"tryCatchPattern":"err := copyAssetDecryptIfEncrypted(src, dst)\nif err != nil {\n    msg := err.Error()\n    if strings.Contains(msg, \"unlock\") {\n        // surface unlock prompt to user, retry after unlock\n    }\n    return err\n}","preventionTips":["Check isBoxUnlockedForAccess before scheduling asset copy/export jobs.","For background/publish flows, defer asset operations until the notebook is confirmed unlocked.","Show a clear UI message naming the locked notebook rather than a generic copy failure."],"tags":["encryption","asset","notebook-lock","fail-closed","export","crypto"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}