{"record":{"id":"d0d0b5f2f171867f","repo":"siyuan-note/siyuan","slug":"312","errorCode":"312","errorMessage":"Encrypted notebook feature is already enabled","messagePattern":"Encrypted notebook feature is already enabled","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/model/crypto.go","lineNumber":986,"sourceCode":"\treturn EnableEncryptedNotebook(password)\n}\n\n// EnableEncryptedNotebook 启用加密笔记本功能：生成 MasterSalt、派生 KEK、写入校验值并持久化。\n// 重复调用（已启用）返回错误，避免覆盖现有加密笔记本的密钥参数。\n// KEK 不缓存——启用后用户需对每个加密笔记本单独调 UnlockBox 解锁。\nfunc EnableEncryptedNotebook(password string) error {\n\tif len(password) == 0 {\n\t\treturn errors.New(\"password must not be empty\")\n\t}\n\n\tnotebookCryptoMu.Lock()\n\tdefer notebookCryptoMu.Unlock()\n\n\tConf.m.RLock()\n\tcurrent := *Conf.NotebookCrypto\n\tConf.m.RUnlock()\n\tif current.Enabled && notebookCryptoConfigurationComplete(&current) {\n\t\treturn errors.New(Conf.Language(312))\n\t}\n\n\thasEncrypted, listErr := hasEncryptedNotebook()\n\tif listErr != nil {\n\t\treturn fmt.Errorf(\"list encrypted notebooks failed: %w\", listErr)\n\t}\n\thasHistory, historyErr := scanEncryptedNotebookHistory()\n\tif historyErr != nil {\n\t\treturn fmt.Errorf(\"check encrypted notebook history failed: %w\", historyErr)\n\t}\n\thasBackup := filelock.IsExist(dataCryptoBackupPath())\n\tif hasEncrypted || hasHistory || hasBackup {\n\t\t// 现存笔记本、已删除笔记本历史或全局备份均表示已有密钥域，必须恢复并认证，不能生成新 MasterSalt。\n\t\tkek, restoreErr := tryRestoreNotebookCryptoFromBackupLocked(password)\n\t\tif kek != nil {\n\t\t\tzeroAndClear(kek)\n\t\t}\n\t\tif restoreErr != nil {","sourceCodeStart":968,"sourceCodeEnd":1004,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/crypto.go#L968-L1004","documentation":"EnableEncryptedNotebook returns Conf.Language(312) ('encrypted notebook feature is already enabled') when the current NotebookCrypto is both Enabled and structurally complete (notebookCryptoConfigurationComplete). The guard prevents a second enable from generating a fresh MasterSalt that would orphan existing WrappedDEKs. This fires before the hasEncrypted/hasHistory/hasBackup recovery path.","triggerScenarios":"EnableEncryptedNotebook (or EnableEncryptedNotebookWithSync falling through to it) is called while Conf.NotebookCrypto.Enabled is true and all required fields are present. Typical when the user clicks 'enable' on a device where encryption is already active.","commonSituations":"User double-invokes enable; UI state desync showing enable as available when it is already on; race where two enable requests are submitted; restored conf.json from a device where encryption was already enabled.","solutions":["Check NotebookCryptoEnabled() before offering the enable action in the UI.","If the intent is to change the master password, use the change-password flow instead of re-enabling.","If the configuration is corrupt-but-enabled, address the corruption directly rather than re-enabling."],"exampleFix":"// before\nerr := model.EnableEncryptedNotebook(password)\n\n// after\nif model.NotebookCryptoEnabled() {\n    // already enabled; route to change-password or unlock instead\n    return\n}\nerr := model.EnableEncryptedNotebook(password)","handlingStrategy":"validation","validationCode":"// Check enabled state before offering the enable action.\nif model.NotebookCryptoEnabled() {\n    // route to unlock / change-password instead of enable\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Gate the 'enable encryption' UI on NotebookCryptoEnabled() returning false.","If the user wants to change credentials, route to the change-master-password flow, not re-enable.","Reflect enabled state in UI immediately after enable succeeds to prevent duplicate submissions."],"tags":["crypto","encryption","state","guard"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}