mihomo-party-org/clash-party · error · Error
Failed to decrypt age ${label}: ${message}
Error message
Failed to decrypt age ${label}: ${message} What it means
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.
Source
Thrown at src/main/utils/age.ts:89
content: string,
secretKey: string | undefined,
label = 'config'
): Promise<string> {
if (!isAgeArmored(content)) return content
const identities = parseAgeSecretKeys(secretKey)
if (identities.length === 0) {
throw new Error(`Age encrypted ${label} requires an age secret key`)
}
try {
const age = await loadAgeModule()
const decrypter = new age.Decrypter()
identities.forEach((identity) => decrypter.addIdentity(identity))
return await decrypter.decrypt(age.armor.decode(content), 'text')
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
throw new Error(`Failed to decrypt age ${label}: ${message}`)
}
}
View on GitHub (pinned to 911e090537)
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.
Example fix
// before: app fails at startup with wrapped decrypt error
const data = await decryptedData()
// after: detect key mismatch and fall back to re-initialization
let data
try {
data = await decryptedData()
} catch (e) {
if (/no identity matched/.test(String(e))) {
await resetEncryptedProfile() // re-encrypt with current identity
data = await decryptedData()
} else {
throw e
}
} Defensive patterns
Strategy: try-catch
Validate before calling
import { existsSync } from 'fs'
// before decrypting, confirm an identity key exists and content looks like age armor
function canAttemptDecrypt(identityPath: string, content: string): boolean {
return existsSync(identityPath) && content.startsWith('-----BEGIN AGE ENCRYPTED FILE-----')
} Type guard
function isAgeArmored(content: string): boolean {
return content.includes('-----BEGIN AGE ENCRYPTED FILE-----') && content.includes('-----END AGE ENCRYPTED FILE-----')
} Try / catch
try {
const plain = await decryptedContent(label)
} catch (e) {
if (/Failed to decrypt age/.test(String(e))) {
// key mismatch or corrupt payload: rebuild/re-encrypt from source of truth
await resetAndReencrypt()
} else {
throw e
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
AI-assisted analysis of mihomo-party-org/clash-party@911e090537 (2026-08-30).
Data as JSON: /api/errors/f347469a02c7686e.
Report an issue: GitHub.