{"record":{"id":"0110d93adeadeb61","repo":"siyuan-note/siyuan","slug":"encrypted-notebook-key-material-is-missing","errorCode":null,"errorMessage":"encrypted notebook key material is missing","messagePattern":"encrypted notebook key material is missing","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/box_conf_crypto.go","lineNumber":37,"sourceCode":"import (\n\t\"errors\"\n\t\"path/filepath\"\n\n\t\"github.com/88250/gulu\"\n\t\"github.com/siyuan-note/filelock\"\n\t\"github.com/siyuan-note/siyuan/kernel/conf\"\n\t\"github.com/siyuan-note/siyuan/kernel/util\"\n)\n\ntype encryptedBoxMetadata struct {\n\tIcon     string `json:\"icon\"`\n\tSort     int    `json:\"sort\"`\n\tSortMode int    `json:\"sortMode\"`\n}\n\nfunc encryptBoxMetadata(boxID string, boxConf *conf.BoxConf, dek []byte) error {\n\tif boxConf == nil || boxConf.BoxCrypt == nil {\n\t\treturn errors.New(\"encrypted notebook key material is missing\")\n\t}\n\tmetadata := &encryptedBoxMetadata{\n\t\tIcon:     filterBoxIcon(boxConf.Icon),\n\t\tSort:     boxConf.Sort,\n\t\tSortMode: boxConf.SortMode,\n\t}\n\tplaintext, err := gulu.JSON.MarshalJSON(metadata)\n\tif err != nil {\n\t\treturn err\n\t}\n\tkey := util.DeriveSubKey(dek, \"siyuan/box-metadata\")\n\tdefer zeroAndClear(key)\n\tboxConf.BoxCrypt.Metadata, err = util.EncryptWithAAD(key, plaintext, boxMetadataAAD(boxID))\n\treturn err\n}\n\nfunc decryptBoxMetadata(boxID string, boxConf *conf.BoxConf, dek []byte) error {\n\tif boxConf == nil || boxConf.BoxCrypt == nil || len(boxConf.BoxCrypt.Metadata) == 0 {","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/box_conf_crypto.go#L19-L55","documentation":"In encryptBoxMetadata (box_conf_crypto.go:35), if boxConf == nil or boxConf.BoxCrypt == nil it returns errors.New('encrypted notebook key material is missing'). encryptBoxMetadata is reached from reuseBoxMetadataIfUnchanged, which only calls it when a DEK is cached (notebook unlocked) but the on-disk encrypted metadata either does not exist or the icon/sort changed so a re-encrypt is needed. The guard therefore fires when the in-memory conf claims encryption is active (a DEK is present) yet the BoxCrypt key material that would hold the ciphertext blob is nil — an inconsistent state.","triggerScenarios":"Box.SaveConf on an unlocked encrypted notebook whose BoxCrypt was cleared/nil'd while a DEK remained cached and the persisted metadata needed (re-)encryption: e.g. a buggy migration that dropped BoxCrypt, a manual edit of conf.json that removed the BoxCrypt object but left Encrypted=true, or a code path that re-armed encryption without re-attaching key material.","commonSituations":"Editing conf.json by hand and stripping BoxCrypt; a partial encryption-enable/disable flow that nil'd the field mid-operation; version upgrade that reshaped BoxCrypt and a stale cached DEK outlived the struct change.","solutions":["Do not hand-edit an encrypted notebook's conf.json — re-establish encryption via the UI/CLI so BoxCrypt and the DEK are set together.","Clear the cached DEK (lock the notebook) before retrying so prepareBoxConfForSave takes the metadata-passthrough branch instead of re-encrypting.","If the notebook is meant to be unencrypted, disable encryption through the proper flow rather than nil-ing BoxCrypt.","Restore conf.json from backup if BoxCrypt was lost, then unlock normally."],"exampleFix":"// before: nil-ing BoxCrypt while leaving Encrypted true\nboxConf.Encrypted = true\nboxConf.BoxCrypt = nil\nbox.SaveConf(boxConf) // -> key material is missing\n\n// after: toggle encryption only through the dedicated flow so key material stays consistent\nif err := model.SetBoxEncryption(boxID, passphrase, enable); err != nil {\n    return err\n}","handlingStrategy":"validation","validationCode":"// Refuse to save an encrypted notebook that lost its key material.\nif conf.Encrypted && conf.BoxCrypt == nil {\n    return errors.New(\"cannot save encrypted notebook without key material\")\n}","typeGuard":null,"tryCatchPattern":"// On this error, lock the notebook (clear the stale DEK) and retry via the proper encryption flow.\nif err := box.SaveConf(conf); err != nil && err.Error() == \"encrypted notebook key material is missing\" {\n    lockNotebook(box.ID)\n    err = reenableEncryption(box.ID, passphrase)\n}","preventionTips":["Never hand-edit an encrypted notebook's conf.json or nil out BoxCrypt.","Change encryption state only through the dedicated enable/disable flow.","If a migration reshapes BoxCrypt, clear cached DEKs so the two cannot drift."],"tags":["encryption","notebook","config","security","crypto"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}