{"record":{"id":"0c4442a25848574b","repo":"agalwood/Motrix","slug":"plugin-lifecycle-secrets-decrypt-failed","errorCode":"plugin.lifecycle.secrets_decrypt_failed","errorMessage":"libsodium decryption failed: ciphertext may be tampered or key mismatch","messagePattern":"libsodium decryption failed: ciphertext may be tampered or key mismatch","errorType":"error_code","errorClass":"SecretStoreError","httpStatus":null,"severity":"critical","filePath":"src/core/plugin/secret-store-libsodium.ts","lineNumber":150,"sourceCode":"      )\n    }\n\n    await sodium.ready\n    const nonce = new Uint8Array(Buffer.from(nonceb64, 'base64'))\n    const ct = new Uint8Array(Buffer.from(ctb64, 'base64'))\n\n    if (nonce.byteLength !== NONCE_BYTES) {\n      throw new SecretStoreError(\n        'plugin.lifecycle.secrets_invalid_token',\n        `secret token nonce must be ${NONCE_BYTES} bytes`\n      )\n    }\n\n    let plainBytes: Uint8Array\n    try {\n      plainBytes = sodium.crypto_secretbox_open_easy(ct, nonce, this.key)\n    } catch {\n      throw new SecretStoreError(\n        'plugin.lifecycle.secrets_decrypt_failed',\n        'libsodium decryption failed: ciphertext may be tampered or key mismatch'\n      )\n    }\n\n    return sodium.to_string(plainBytes)\n  }\n}\n","sourceCodeStart":132,"sourceCodeEnd":159,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/secret-store-libsodium.ts#L132-L159","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — lockbox regenerated, old tokens undecryptable\n// userDataDir/secrets.lockbox was deleted -> new random key\nawait store.decrypt(oldToken)  // throws secrets_decrypt_failed\n\n// after — restore the original lockbox (or re-enter secrets)\n// 1) stop the app, restore the backed-up secrets.lockbox to userDataDir\n// 2) restart; decrypt now uses the matching key\nawait store.decrypt(oldToken)  // succeeds","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  return await store.decrypt(token)\n} catch (e) {\n  if (e instanceof SecretStoreError && e.code === 'plugin.lifecycle.secrets_decrypt_failed') {\n    // Key mismatch or tampering. Try to restore the original key source\n    // (MOTRIX_SECRETS_SEED or <userDataDir>/secrets.lockbox); if unrecoverable,\n    // prompt the user to re-enter the secret and re-encrypt.\n    return await recoverSecretAndReencrypt(token)\n  }\n  throw e\n}","preventionTips":["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."],"tags":["plugin","secrets","libsodium","crypto","key-mismatch","data-loss"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}