agalwood/Motrix · critical · SecretStoreError
plugin.lifecycle.secrets_decrypt_failed
plugin.lifecycle.secrets_decrypt_failed
Error message
libsodium decryption failed: ciphertext may be tampered or key mismatch
What it means
Thrown by LibsodiumSecretStore.decrypt: the token was structurally valid and the nonce was 24 bytes, but `sodium.crypto_secretbox_open_easy(ct, nonce, key)` threw. crypto_secretbox is authenticated — failure means the ciphertext/Auth tag did not validate under the current key. The two real causes are a tampered/corrupted ciphertext, or a key mismatch (the key used to decrypt differs from the one used to encrypt).
Source
Thrown at src/core/plugin/secret-store-libsodium.ts:150
)
}
await sodium.ready
const nonce = new Uint8Array(Buffer.from(nonceb64, 'base64'))
const ct = new Uint8Array(Buffer.from(ctb64, 'base64'))
if (nonce.byteLength !== NONCE_BYTES) {
throw new SecretStoreError(
'plugin.lifecycle.secrets_invalid_token',
`secret token nonce must be ${NONCE_BYTES} bytes`
)
}
let plainBytes: Uint8Array
try {
plainBytes = sodium.crypto_secretbox_open_easy(ct, nonce, this.key)
} catch {
throw new SecretStoreError(
'plugin.lifecycle.secrets_decrypt_failed',
'libsodium decryption failed: ciphertext may be tampered or key mismatch'
)
}
return sodium.to_string(plainBytes)
}
}
View on GitHub (pinned to 1a708ee577)
Solutions
- Restore the matching key: re-supply the same MOTRIX_SECRETS_SEED env var, or restore the original <userDataDir>/secrets.lockbox file.
- If the plaintext is recoverable elsewhere, re-encrypt it under the current key and overwrite the stored token.
- If the key is permanently lost, accept the secrets are unrecoverable, re-enter them through the normal config UI so they are re-encrypted.
- Back up secrets.lockbox alongside the secrets data so key and ciphertexts always travel together.
Example fix
// before — lockbox regenerated, old tokens undecryptable // userDataDir/secrets.lockbox was deleted -> new random key await store.decrypt(oldToken) // throws secrets_decrypt_failed // after — restore the original lockbox (or re-enter secrets) // 1) stop the app, restore the backed-up secrets.lockbox to userDataDir // 2) restart; decrypt now uses the matching key await store.decrypt(oldToken) // succeeds
Defensive patterns
Strategy: try-catch
Try / catch
try {
return await store.decrypt(token)
} catch (e) {
if (e instanceof SecretStoreError && e.code === 'plugin.lifecycle.secrets_decrypt_failed') {
// Key mismatch or tampering. Try to restore the original key source
// (MOTRIX_SECRETS_SEED or <userDataDir>/secrets.lockbox); if unrecoverable,
// prompt the user to re-enter the secret and re-encrypt.
return await recoverSecretAndReencrypt(token)
}
throw e
} Prevention
- Back up <userDataDir>/secrets.lockbox wherever you back up secret data; restore it together with the encrypted tokens.
- Pin MOTRIX_SECRETS_SEED across environments that must read each other's tokens.
- Use the same key-resolution path in every runtime that reads a given field (env-seed vs lockbox must agree).
- Treat secrets_decrypt_failed as potential key loss — surface a re-entry flow, never silently swallow.
When it happens
Trigger: decrypt(token) where the format/nonce checks pass but the AEAD verification fails. Most common when the secrets.lockbox file was regenerated (new random key) or MOTRIX_SECRETS_SEED changed between encrypt and decrypt runs.
Common situations: userDataDir was reset/moved so a fresh secrets.lockbox was generated (Priority 3 in create()). envSeed changed because the deployment env var was rotated. Same plaintext encrypted under one runtime's key then decrypted under another (Electron vs server) where key sources differ. Backup restored secrets without restoring the lockbox.
Related errors
- plugin.lifecycle.secrets_invalid_token
- plugin.lifecycle.secrets_seed_missing
- plugin.commands.id_out_of_namespace
- plugin.command.not_declared_in_manifest
- plugin.commands.not_found
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/0c4442a25848574b.
Report an issue: GitHub.