{"record":{"id":"c7eed877501b0f11","repo":"siyuan-note/siyuan","slug":"cannot-write-incomplete-notebook-crypto-backup","errorCode":null,"errorMessage":"cannot write incomplete notebook crypto backup","messagePattern":"cannot write incomplete notebook crypto backup","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/crypto.go","lineNumber":421,"sourceCode":"\tif err != nil {\n\t\treturn fmt.Errorf(\"marshal notebook crypto backup failed: %w\", err)\n\t}\n\tif err := atomicWriteFile(backupPath, data); err != nil {\n\t\treturn fmt.Errorf(\"write notebook crypto backup failed: %w\", err)\n\t}\n\treturn nil\n}\n\n// writeNotebookCryptoBackupData 将指定的 NotebookCrypto 写入备份文件（不依赖 Conf.NotebookCrypto）。\n// kek 必须非 nil：在 Checksum 定型后计算 KEKMAC，保证落盘 MAC 与落盘内容一致。\nfunc writeNotebookCryptoBackupData(nc *conf.NotebookCrypto, kek []byte) error {\n\tif kek == nil {\n\t\treturn errors.New(\"cannot generate notebook crypto backup without KEK\")\n\t}\n\tprepareBackupForWrite(nc)\n\tnc.KEKMAC = computeKEKMAC(nc, kek)\n\tif !notebookCryptoConfigurationComplete(nc) {\n\t\treturn errors.New(\"cannot write incomplete notebook crypto backup\")\n\t}\n\tbackupPath := dataCryptoBackupPath()\n\tif err := os.MkdirAll(filepath.Dir(backupPath), 0755); err != nil {\n\t\treturn fmt.Errorf(\"mkdir notebook crypto backup dir failed: %w\", err)\n\t}\n\tdata, err := json.Marshal(nc)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"marshal notebook crypto backup failed: %w\", err)\n\t}\n\tif err := atomicWriteFile(backupPath, data); err != nil {\n\t\treturn fmt.Errorf(\"write notebook crypto backup failed: %w\", err)\n\t}\n\treturn nil\n}\n\n// verifyKEKAgainstExistingBoxes 用 KEK 对所有现有加密笔记本的 WrappedDEK 做无副作用解密校验。\n// 优先尝试 conf 的 WrappedDEK，解密失败时 fallback 到 backup（与解锁路径一致）；\n// GetBoxEncryption 报错时 fail-closed（元数据损坏的加密笔记本不能静默跳过）。","sourceCodeStart":403,"sourceCodeEnd":439,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/crypto.go#L403-L439","documentation":"writeNotebookCryptoBackupData refuses to persist a notebook-crypto backup whose configuration is incomplete after stamping the KEKMAC. The library treats a partially initialized NotebookCrypto config as unsafe to back up because restoring from it would leave encryption in a broken state. It throws this sentinel error before any file I/O happens.","triggerScenarios":"Calling ImportNotebookCryptoBackup or triggering tryRestoreNotebookCryptoFromBackupLocked when the resulting conf.NotebookCrypto is missing required fields (e.g. no WrappedDEK, salt, or other mandatory configuration) after prepareBackupForWrite/computeKEKMAC run.","commonSituations":"A hand-edited or truncated conf.NotebookCrypto JSON, a backup imported from an older/incompatible version missing new spec fields, or enabling notebook encryption where key derivation partially failed leaving nil/empty components.","solutions":["Inspect the NotebookCrypto config with notebookCryptoConfigurationComplete to find which required field is empty before writing","Re-import or regenerate the backup from a complete configuration (complete WrappedDEK, salt, KEKMAC)","Ensure the KEK was correctly derived (deriveKEK succeeded) before attempting backup writes","Restore the workspace conf from a known-good backup or re-run the master password setup flow"],"exampleFix":"// before: writing a partially-populated struct\nnc := &conf.NotebookCrypto{Spec: conf.CurrentNotebookCryptoSpec}\nerr := writeNotebookCryptoBackupData(nc, kek) // fails: incomplete\n// after: populate required fields first\nnc := &conf.NotebookCrypto{Spec: conf.CurrentNotebookCryptoSpec, WrappedDEK: wrapped, Salt: salt, AutoLock: autoLock}\nnc.KEKMAC = computeKEKMAC(nc, kek)\nif !notebookCryptoConfigurationComplete(nc) { return errors.New(\"refusing: incomplete config\") }\nerr := writeNotebookCryptoBackupData(nc, kek)","handlingStrategy":"validation","validationCode":"if !notebookCryptoConfigurationComplete(nc) {\n    return fmt.Errorf(\"backup skipped: incomplete notebook crypto config\")\n}\nif kek == nil {\n    return fmt.Errorf(\"backup skipped: KEK not derived\")\n}","typeGuard":"func backupWritable(nc *conf.NotebookCrypto, kek []byte) bool {\n    return kek != nil && notebookCryptoConfigurationComplete(nc)\n}","tryCatchPattern":null,"preventionTips":["Always derive the KEK before attempting any backup write","Run notebookCryptoConfigurationComplete as a precondition in callers, not just inside the writer","Never persist a NotebookCrypto struct assembled from partial user input","Validate configs after import/migration before touching the backup path"],"tags":["go","encryption","notebook-crypto","backup"],"backgroundTag":"missing-required-config-field","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}