{"record":{"id":"a6391cfb054f3fb2","repo":"siyuan-note/siyuan","slug":"310","errorCode":"310","errorMessage":"Encrypted notebook feature is not enabled","messagePattern":"Encrypted notebook feature is not enabled","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"kernel/model/crypto.go","lineNumber":1172,"sourceCode":"\t*Conf.NotebookCrypto = *backup\n\tConf.m.Unlock()\n\tConf.Save()\n\t// 恢复成功后同步重写备份，确保配置和备份内容一致。\n\t// 调用方已持有 notebookCryptoMu，且 writeNotebookCryptoBackupData 不再申请该锁，故无死锁；\n\t// 同步写避免与 ChangeMasterPassword 的并发备份写竞争同一文件（lost update 导致 verifier 被回退）。\n\tnc := *backup\n\tif err := writeNotebookCryptoBackupData(&nc, kek); err != nil {\n\t\tlogging.LogWarnf(\"rewrite notebook crypto backup after restore failed: %s\", err)\n\t}\n\tlogging.LogInfof(\"notebook crypto restored from backup (e.g. after sync to a new device)\")\n\treturn kek, nil\n}\n\n// deriveNotebookCryptoBackupCandidate 对同步备份做无副作用验证，并确认它覆盖全部现有加密笔记本。\nfunc deriveNotebookCryptoBackupCandidate(password string) (backup *conf.NotebookCrypto, kek []byte, err error) {\n\tbackup, err = loadNotebookCryptoBackup()\n\tif err != nil || backup == nil || len(backup.MasterSalt) == 0 || len(backup.KEKVerifier) == 0 {\n\t\treturn nil, nil, errors.New(Conf.Language(310))\n\t}\n\tparams, validErr := util.ValidateArgon2Params(backup.KDFParams)\n\tif validErr != nil {\n\t\treturn nil, nil, errors.New(Conf.Language(317))\n\t}\n\tkek = util.DeriveKey(password, backup.MasterSalt, params)\n\tdecrypted, decryptErr := util.DecryptWithAAD(kek, backup.KEKVerifier, []byte(\"siyuan:kek-verifier\"))\n\tif decryptErr != nil || string(decrypted) != string(kekVerifierMagic) {\n\t\tzeroAndClear(kek)\n\t\treturn nil, nil, errors.New(Conf.Language(311))\n\t}\n\tif backup.Spec != conf.CurrentNotebookCryptoSpec || backup.Checksum == \"\" ||\n\t\tlen(backup.KEKMAC) == 0 || !verifyKEKMAC(backup, kek) {\n\t\tzeroAndClear(kek)\n\t\treturn nil, nil, errors.New(Conf.Language(316))\n\t}\n\tif !verifyKEKAgainstExistingBoxes(kek) || !verifyKEKAgainstEncryptedHistory(kek) {\n\t\tzeroAndClear(kek)","sourceCodeStart":1154,"sourceCodeEnd":1190,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/crypto.go#L1154-L1190","documentation":"Thrown by deriveNotebookCryptoBackupCandidate (crypto.go:1172, i18n code 310) when the global key backup is unusable as a recovery source: loadNotebookCryptoBackup errored, or returned nil, or its MasterSalt/KEKVerifier fields are empty. This candidate function is called during recovery (new-device sync, re-enable, deriveKEK fallback), so an absent backup means there is no key material to verify the password against. The message 'Encrypted notebook feature is not enabled' reflects that no authenticated crypto config is available.","triggerScenarios":"Reached when: (a) EnableEncryptedNotebook finds a key domain and calls tryRestoreNotebookCryptoFromBackupLocked -> deriveNotebookCryptoBackupCandidate, or (b) deriveKEK with local config disabled/invalid tries the backup. The backup file at dataCryptoBackupPath() is missing, deleted, unreadable, or lacks MasterSalt/KEKVerifier.","commonSituations":"Sync delivered encrypted notebooks but the key backup file did not arrive or was deleted. Backup was manually removed. A partially-written backup after a crash left it JSON-valid but missing key fields.","solutions":["Restore the key backup file (dataCryptoBackupPath()) from a sync snapshot, external copy, or another device that has it, then retry.","If the local conf.json still has MasterSalt/KEKVerifier, rely on the local-config auth path instead (ensure conf is not marked Enabled=false so deriveKEK uses it), or re-enable via conf recovery.","If no key material exists anywhere and the encrypted data is expendable, remove the orphaned encrypted notebooks/history so the key-domain check no longer blocks a fresh enable."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before relying on backup recovery, confirm the backup is present and populated.\nfunc backupUsable() error {\n    if !filelock.IsExist(dataCryptoBackupPath()) {\n        return errors.New(\"key backup file missing; restore it before recovery\")\n    }\n    b, err := loadNotebookCryptoBackup()\n    if err != nil || b == nil || len(b.MasterSalt) == 0 || len(b.KEKVerifier) == 0 {\n        return errors.New(\"key backup incomplete; restore a full backup\")\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"if err := model.EnableEncryptedNotebook(password); err != nil {\n    if err.Error() == model.Conf.Language(310) {\n        respond(c, \"no usable key backup on this device; restore the backup or sync it here\")\n        return\n    }\n    respond(c, err.Error())\n}","preventionTips":["Always let the global key backup sync to every device.","Keep an external copy of the key backup file.","After Data.zip import, verify the backup file landed before unlocking."],"tags":["encryption","notebook-crypto","key-recovery","backup"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}