{"record":{"id":"f347469a02c7686e","repo":"mihomo-party-org/clash-party","slug":"failed-to-decrypt-age-label-message","errorCode":null,"errorMessage":"Failed to decrypt age ${label}: ${message}","messagePattern":"Failed to decrypt age (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/utils/age.ts","lineNumber":89,"sourceCode":"  content: string,\n  secretKey: string | undefined,\n  label = 'config'\n): Promise<string> {\n  if (!isAgeArmored(content)) return content\n\n  const identities = parseAgeSecretKeys(secretKey)\n  if (identities.length === 0) {\n    throw new Error(`Age encrypted ${label} requires an age secret key`)\n  }\n\n  try {\n    const age = await loadAgeModule()\n    const decrypter = new age.Decrypter()\n    identities.forEach((identity) => decrypter.addIdentity(identity))\n    return await decrypter.decrypt(age.armor.decode(content), 'text')\n  } catch (error) {\n    const message = error instanceof Error ? error.message : String(error)\n    throw new Error(`Failed to decrypt age ${label}: ${message}`)\n  }\n}\n","sourceCodeStart":71,"sourceCodeEnd":92,"githubUrl":"https://github.com/mihomo-party-org/clash-party/blob/911e090537acdf7c50bee1c3aebecc2ef119a8b5/src/main/utils/age.ts#L71-L92","documentation":"decryptAgeContent wraps any failure from the age decryption library (decrypter.decrypt on armored content) into a labeled error 'Failed to decrypt age <label>: <detail>'. It exists so callers (decryptedData, profile, decryptedContent) can tell which encrypted blob failed, since age itself only returns low-level crypto errors. The inner message from the age library (e.g. 'no identity matched any of the recipients') is preserved in the wrapped message.","triggerScenarios":"Calling decryptAgeContent (directly or via decryptedData/profile/decryptedContent) with content that was not produced by the matching age.encrypt/armor pipeline, an identity key that does not correspond to the file's recipient, or corrupted/truncated armored text.","commonSituations":"User restores an encrypted profile from a backup but the age identity/private key file was regenerated or is from another machine; app upgrade changes key storage location; file partially written during crash/disk-full; user hand-edits the armored payload.","solutions":["Verify the age identity/private key file on disk matches the one used when the content was encrypted (regenerating keys invalidates old ciphertext).","Re-export or re-encrypt the data from its source of truth (e.g. re-save the profile so it is encrypted with the current identity).","Inspect the inner message after the colon: 'no identity matched any of the recipients' means wrong key; malformed armor/EOF means the file itself is corrupt.","Check file permissions and that the identity path passed into identities is readable by the app process."],"exampleFix":"// before: app fails at startup with wrapped decrypt error\nconst data = await decryptedData()\n// after: detect key mismatch and fall back to re-initialization\nlet data\ntry {\n  data = await decryptedData()\n} catch (e) {\n  if (/no identity matched/.test(String(e))) {\n    await resetEncryptedProfile() // re-encrypt with current identity\n    data = await decryptedData()\n  } else {\n    throw e\n  }\n}","handlingStrategy":"try-catch","validationCode":"import { existsSync } from 'fs'\n// before decrypting, confirm an identity key exists and content looks like age armor\nfunction canAttemptDecrypt(identityPath: string, content: string): boolean {\n  return existsSync(identityPath) && content.startsWith('-----BEGIN AGE ENCRYPTED FILE-----')\n}","typeGuard":"function isAgeArmored(content: string): boolean {\n  return content.includes('-----BEGIN AGE ENCRYPTED FILE-----') && content.includes('-----END AGE ENCRYPTED FILE-----')\n}","tryCatchPattern":"try {\n  const plain = await decryptedContent(label)\n} catch (e) {\n  if (/Failed to decrypt age/.test(String(e))) {\n    // key mismatch or corrupt payload: rebuild/re-encrypt from source of truth\n    await resetAndReencrypt()\n  } else {\n    throw e\n  }\n}","preventionTips":["Back up the age identity key alongside (or securely with) the encrypted data so they never desync","Re-encrypt data after any key regeneration or migration","Validate armored payload structure before decryption attempts","Never hand-edit encrypted files; always go through the app's save path"],"tags":["crypto","decryption","age","file-corruption"],"backgroundTag":"decryption-failed-no-matching-identity","analyzedSha":"911e090537acdf7c50bee1c3aebecc2ef119a8b5","analyzedAt":"2026-08-30T13:00:49.174Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}