Is Mask Network safe?

Medium risk

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.

Dimensionv2.37.0Chrome Web Store
45Risk
Who publishes it

Dimension - no other listings under this identity, 18 shared hostnames

What this publisher told the store about itself, and the other listings that told it the same thing.

Shared hosts - 18 hostnames

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

api.lens.xyz
Also called by 1 other listing: Focalize - Extension for Lens
api.staging.lens.dev
Also called by 1 other listing: Focalize - Extension for Lens
api.transak.com
Also called by 1 other listing: Compass Wallet
api.web3.bio
Also called by 1 other listing: Grove - Get paid to create, anywhere.
developers.flow.com
Also called by 1 other listing: Soda
hub.snapshot.org
Also called by 1 other listing: Talken Wallet
public.rpc.solanavibestation.com
Also called by 1 other listing: Rise.Rich Local Remarks
seq.snapshot.org
Also called by 1 other listing: Talken Wallet
docs.onflow.org
Also called by 2 other listings, including Soda
minds.com
Also called by 3 other listings
snapshot.org
Also called by 3 other listings, including Talken Wallet
api.rabby.io
Also called by 4 other listings
cdn.stamp.fyi
Also called by 4 other listings, including Talken Wallet, Focalize - Extension for Lens
ens.domains
Also called by 4 other listings
gopluslabs.io
Also called by 4 other listings
static.okx.com
Also called by 5 other listings
sub.domain.co.uk
Also called by 5 other listings
api.prd.space.id
Also called by 6 other listings

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

MediumunwantedCWE-522Dynamic sandbox

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.

What actually happens

You did this

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 did this

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.

The code that does this

Six-digit key derivation, hardcoded IV, and the upload

Readable version

Readable form of the scheme

// 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 }

Session material inside the encrypted payload

  • 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.

Why you can't catch this in DevTools

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.

Decoded value
{  "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..."    }  ]}

Reproduce it yourself

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.

Requires
  • Node.js 18+
firefly-sync-bruteforce.js · js
#!/usr/bin/env nodeconst crypto = require('crypto');const APP_LOGIN_ENCRYPT_IV = '0x4f05c37c16c801c2516b0338a8fd0cf9';const iv = Buffer.from(APP_LOGIN_ENCRYPT_IV.replace(/^0x/, ''), 'hex');// generateCryptoKey() from the extensionfunction 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 IVfunction 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);
How to run it
  1. 1Save as firefly-sync-bruteforce.js.
  2. 2node firefly-sync-bruteforce.js.
  3. 3Observe the random 6-digit key recovered and the cookie payload decrypted in a few seconds.

The key is also disclosed outside the ciphertext

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.

Where it sends data

Destinations our analysis observed Mask Network contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • firefly.social

    Mask Network sends data to firefly.social. No other extension we have analyzed sends data here.

Updated 2 November 2026jkoeaghipilijlahjplgbfiocjhldnap