{"record":{"id":"2c4efe4160fee529","repo":"siyuan-note/siyuan","slug":"w-v-crypto","errorCode":null,"errorMessage":"%w: %v","messagePattern":"%w: %v","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/crypto.go","lineNumber":1310,"sourceCode":"\t\t\tlogging.LogInfof(\"repaired notebook crypto configuration from authenticated backup\")\n\t\t} else if !backupAuthenticated {\n\t\t\t// 同步备份可能属于另一轮完整改密；只要本地配置仍与全部笔记本一致，就继续使用本地配置，\n\t\t\t// 不覆盖候选备份，等待其余 WrappedDEK 同步完成后由新密码采用。\n\t\t\tlogging.LogWarnf(\"notebook crypto backup differs from usable local configuration; keeping both candidates\")\n\t\t}\n\t}\n\n\tif migrationPending {\n\t\t// 崩溃恢复后的首次新密码验证：确认所有笔记本都已切换到新 KEK，再生成带认证的全局备份并结束迁移。\n\t\tkeys, keyErr := decryptHistoryKEKs(kek, nc.HistoryKEKs)\n\t\tclearHistoryKEKs(keys)\n\t\tif keyErr != nil || !verifyKEKAgainstExistingBoxes(kek, nil) || !verifyKEKAgainstEncryptedHistory(kek, &nc) {\n\t\t\tzeroAndClear(kek)\n\t\t\treturn nil, errMasterPasswordMigrationPending\n\t\t}\n\t\tif err = saveNotebookCryptoBackup(kek); err != nil {\n\t\t\tzeroAndClear(kek)\n\t\t\treturn nil, fmt.Errorf(\"%w: %v\", errMasterPasswordMigrationPending, err)\n\t\t}\n\t\tremoveMasterPasswordMigration()\n\t}\n\treturn kek, nil\n}\n\n// decryptBoxCrypt 用 KEK 解密 box 的 WrappedDEK。优先使用 GetBoxEncryption 的结果（conf → backup fallback），\n// 若解密失败则尝试 backup 中不同的 WrappedDEK。\n// 返回解密后的 DEK 和实际使用的 BoxCrypt（可能来自 backup）。\n// 若 backup 被使用会自动修复 conf.json 和刷新 backup。\nfunc decryptBoxCrypt(boxID string, kek []byte) (dek []byte, boxCrypt *conf.BoxEncryption, err error) {\n\tboxCrypt, err = GetBoxEncryption(boxID)\n\tif err != nil || boxCrypt == nil || len(boxCrypt.WrappedDEK) == 0 {\n\t\treturn nil, nil, fmt.Errorf(\"no encrypted key material for box [%s]\", boxID)\n\t}\n\n\tnc := currentNotebookCrypto()\n\tdek, err = decryptWrappedDEKWithHistory(boxID, boxCrypt, kek, nc)","sourceCodeStart":1292,"sourceCodeEnd":1328,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/crypto.go#L1292-L1328","documentation":"deriveKEK wraps errMasterPasswordMigrationPending (with the underlying cause via %w/%v) when a master-password migration is still in progress. During migration, the KEK is only accepted after all notebooks and encrypted history have been re-wrapped and a new authenticated global crypto backup can be saved. If saving that backup fails, or the re-wrap verification fails, derivation aborts because the old/new password transition is not yet complete.","triggerScenarios":"Calling UnlockBox/UnlockAndMountBox or ChangeMasterPassword (and test paths) with the new password while a master-password migration marker exists and either decryptHistoryKEKs/verifyKEKAgainst* fails (returns bare errMasterPasswordMigrationPending) or saveNotebookCryptoBackup returns a write error (returns wrapped '%w: %v').","commonSituations":"A previous ChangeMasterPassword was interrupted (crash/power loss) leaving the migration marker on disk; the workspace backup directory is not writable or on a full disk; synced machines have partially migrated WrappedDEKs so verification against all boxes fails.","solutions":["Complete the pending master-password migration: call ChangeMasterPassword again with the correct password so all notebooks/history are re-wrapped and the migration marker is removed","Fix the underlying cause in the wrapped error (%v part): free disk space or repair permissions for the workspace crypto backup path","If verification keeps failing, ensure every encrypted notebook is present and synced; a missing notebook prevents migration completion","As last resort restore the workspace from a backup taken before the interrupted migration"],"exampleFix":"// before\nkek, err := deriveKEK(password)\nif errors.Is(err, model.ErrMasterPasswordMigrationPending) {\n    return err // treated as wrong password, user stuck\n}\n// after\nif errors.Is(err, model.ErrMasterPasswordMigrationPending) {\n    // retry ChangeMasterPassword/verify with the same password to finish migration,\n    // surface the %v cause to the user for actionable feedback\n    return fmt.Errorf(\"migration pending: %w\", err)\n}","handlingStrategy":"try-catch","validationCode":"if model.IsMasterPasswordMigrationPending() {\n    return errors.New(\"complete pending master-password migration before unlocking\")\n}","typeGuard":null,"tryCatchPattern":"kek, err := model.DeriveKEKForTest(password)\nif err != nil {\n    if errors.Is(err, model.ErrMasterPasswordMigrationPending) {\n        // surface cause via errors.Unwrap and retry ChangeMasterPassword\n    }\n}","preventionTips":["Avoid killing the process during ChangeMasterPassword","Monitor errors.Unwrap(err) for the underlying backup-write cause","Ensure workspace disk has free space before password changes","Keep all encrypted notebooks synced before migrating passwords"],"tags":["encryption","migration","key-derivation","go"],"backgroundTag":"invalid-state-transition","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}