Is VibeMate safe?
VibeMate stays dormant until you open a livestream cam site, then reads the page's traffic and forwards chat and tip data to its servers.
On configured adult cam sites (Chaturbate, Stripchat, BongaCams, Cam4, CamSoda, MyFreeCams), VibeMate injects a page script that replaces the site's WebSocket, XMLHttpRequest, Worker and fetch with wrappers that read every response body. It parses out tip events — model name, tip amount and the tipping viewer's username, including other viewers' data — and relays them through an offscreen iframe to fanberry.com. The extension keeps its features hidden until it detects one of these sites, and the list of targeted sites is stored AES-encrypted and can be expanded remotely via messages from fanberry.com or vibemate.com without an update.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
VibeMate hides cam-site features until a cam site is visited
VibeMate ships locked, withholding cam tools until you open a cam site, fanberry.com, or lovense.com.
Opening one triggers a check against an encrypted site list, an unlock flag, a log to fanberry.com, and an uninstall-URL update.
You open any browser tab on a cam site, fanberry.com, or lovense.com.
The extension checks open tabs against its encrypted cam-site list, marks the session unlocked, logs the event, and updates the uninstall URL to record the visit.
This check happens automatically without any user action inside the extension.
Undisguise routine, background.js E() (deobfuscated)
// E() — conditional unlock routine
async function E() {
// Short-circuit if already unlocked
if (await r.getItem('local:isShowMoreFeature')) return;
// Log initial disguised state once
if (!await r.getItem('local:isSendLockLog')) {
f.add({ eventId: 'disguise', content: { position: 'sidepanel', click_type: 'lock' } });
await r.setItem('local:isSendLockLog', true);
}
await k(5000); // brief delay
let unlockReason = '';
const camSitePatterns = await P(); // decrypted site list from AES blob
const openUrls = (await n.tabs.query({})).map(t => t.url);
let matched = false;
for (const url of openUrls) {
const isCamSite = camSitePatterns.some(p => S(p, url));
const isFanberry = /^https:\/\/www\.fanberry\.com\/[^/?#].*$/.test(url) && !url.includes('uninstall');
const isLovense = (new URL(url).host || '').includes(N); // N = decrypted 'lovense.com'
if (isCamSite) unlockReason = 'camsite';
else if (isFanberry) unlockReason = 'fanberry';
else if (isLovense) unlockReason = 'lovense';
if (isCamSite || isFanberry || isLovense) { matched = true; break; }
}
if (matched) {
await r.setItem('local:isShowMoreFeature', true); // persist unlocked
// Log unlock sequence
f.add({ eventId: 'disguise', content: { click_type: 'unlock' } });
f.add({ eventId: 'disguise', content: { click_type: 'undisguised', click_name: unlockReason } });
v(); // update uninstall URL with s=1
m('UPDATE_UNDISGUISED', true);
}
}Uninstall URL encodes whether you ever visited a cam site
// v() — updates the uninstall URL to record whether the user reached a cam site
function v() {
let url = 'https://www.fanberry.com/uninstall';
const version = '1.4.4';
r.getItem('local:isShowMoreFeature').then(unlocked => {
// s=1 means user visited a cam site; s=0 means they never did
url += unlocked
? `?s=1&v=${version}&t=${Date.now()}`
: `?s=0&v=${version}&t=${Date.now()}`;
n.runtime.setUninstallURL(url); // registered as the uninstall beacon
});
}The list of cam sites where the extension activates ships AES-CBC encrypted. Only the extension runtime can read which sites trigger the unlock.
The encrypted blob (variable q in background.js, decrypted by V1() from chunks/index-C0YbUdoZ.js using AES-CBC, key = last 16 chars of blob, IV = reversed 16 chars after the length digit) decodes to a JSON object mapping platform names to arrays of URL match patterns — the full list of cam sites that trigger the unlock. dynamic analysis confirmed this list includes chaturbate.com patterns among others.
- www.fanberry.com
Receives the uninstall beacon (GET /uninstall?s=1&v=1.4.4&t=...) encoding whether you ever visited a cam site; also the base domain for the extension's operator.
- customer-api.fanberry.com
Receives AES-encrypted BI log events (POST /api/camlient/log/report) including the disguise/unlock event sequence. Confirmed by dynamic analysis.
VibeMate intercepts all cam-site WebSocket and HTTP traffic
On cam sites VibeMate supports, base.js replaces WebSocket, XHR, fetch, and Worker, then forwards tip events (model, amount, sender) to an offscreen fanberry.com iframe from Chaturbate, Stripchat, BongaCams, Cam4, CamSoda, MyFreeCams.
You open a cam site that VibeMate supports (e.g. Chaturbate, Stripchat, BongaCams).
base.js replaces the page's WebSocket, XMLHttpRequest, fetch, and Worker, forwarding captured responses to an offscreen fanberry.com iframe.
This happens on every page load once the extension's cam-site list matches the current tab URL.
J1(window), WebSocket and fetch override in base.js (lines 2131-2222)
// J1(window) — installed by base.js on every cam-site page
function J1(n) {
// --- WebSocket: capture every incoming message ---
n.WebSocket = class extends n.WebSocket {
constructor(...args) {
super(...args);
const socketId = `lvs-ws-${randomId(8)}`;
this.addEventListener('open', () => { openSockets[socketId] = this; });
this.addEventListener('close', () => { delete openSockets[socketId]; });
this.addEventListener('message', evt => {
X('LVS_VIEWER_WEB_SOCKET_MESSAGE', {
socketId, socketUrl: this.url,
messageType: 'ws', platform: currentPlatform,
data: evt?.data // ← full message payload
});
});
}
};
// --- XMLHttpRequest: capture completed responses ---
n.XMLHttpRequest = class extends n.XMLHttpRequest {
constructor(...args) {
super(...args);
this.addEventListener('readystatechange', () => {
if (this.readyState !== 4 || this.responseType === 'arraybuffer') return;
X('LVS_VIEWER_XHR_MESSAGE', {
messageType: 'http',
data: { readyState: 4, response: this.response,
responseURL: this.responseURL || location.origin,
responseType: this.responseType, status: this.status }
});
}, false);
}
};
// --- fetch: clone every response body (JSON or text) ---
const origFetch = n.fetch;
n.fetch = async (...args) => {
const resp = await origFetch(...args);
const clone = resp.clone();
let body; let type = '';
try { body = await resp.clone().json(); type = 'json'; } catch {}
if (!body) try { body = await resp.clone().text(); type = 'text'; } catch {}
if (body) X('LVS_VIEWER_XHR_MESSAGE', {
messageType: 'http',
data: { readyState: 4, response: body, responseURL: new URL(args[0], location.origin).href,
responseType: type, status: clone.status }
});
return resp; // original response returned to page
};
// --- Worker messages ---
n.Worker = class extends n.Worker {
constructor(...args) {
super(...args);
this.addEventListener('message', evt => {
X('LVS_VIEWER_WORKER_MESSAGE', { data: evt?.data });
});
}
};
// ServiceWorker messages
if ('serviceWorker' in navigator)
navigator.serviceWorker.addEventListener('message', evt => {
X('LVS_VIEWER_SERVICE_WORKER_MESSAGE', evt);
});
}Tip event parsing for 6 cam platforms, offscreen-8cn6P2JZ.js TipHandler
// Platform-name constants — each DTX$ blob decrypts to the cam-site domain name
const PLATFORMS = {
CB: decrypt('DTX$71yjytze...'), // → 'chaturbate.com'
SC: decrypt('DTX$71hRYfow...'), // → 'stripchat.com'
BC: decrypt('DTX$31SWJGIE...'), // → 'bongacams.com'
C4: decrypt('DTX$51sOaDID...'), // → 'cam4.com'
CS: decrypt('DTX$31pbIV6B...'), // → 'camsoda.com'
MF: decrypt('DTX$612mvQ4x...'), // → 'myfreecams.com'
};
class TipHandler {
handle(event) {
// Match event.platform to a key; call the platform-specific parser
const key = Object.keys(PLATFORMS).find(k => PLATFORMS[k] === event.platform);
if (key) this[key](event);
}
async dispatch(tipInfo) {
// Fires LVS_VIEWER_TIP_MESSAGE — picked up by the offscreen iframe relay
E('LVS_VIEWER_TIP_MESSAGE', tipInfo);
}
CB({ data, platform, sender }) {
if (!data.includes('RoomTipAlertTopic')) return;
const msg = JSON.parse(JSON.parse(data).messages[0].data);
this.dispatch({
tabId: sender.tab?.id, platform,
modelName: msg.to_username,
amount: msg.amount,
viewer: msg.from_username, // ← the tipping viewer's name
time: Date.now()
});
}
// SC, BC, C4, CS, MF follow the same shape:
// extract { modelName, amount, viewer } and dispatch
}| Field | Value | Why it matters | |
|---|---|---|---|
Model name | sweetkittyx | The screen name of the performer receiving the tip, visible to anyone monitoring the data. | |
Tipping viewer username | tipperlover99 | The screen name of the viewer who sent the tip, another user's identity disclosed without their knowledge. | |
Tip amount (tokens) | 250 | The number of tokens in the tip, revealing financial transaction details for third-party viewers. | |
Event timestamp | 1749254832471 | Unix millisecond timestamp of when the tip occurred. | |
Platform identifier | chaturbate.com | The cam site domain the event came from, used to route to the correct parser. |
- www.fanberry.com
Hosts the offscreen iframe at /extension/<id>/background.html. Intercepted traffic and parsed tip events post into it via postMessage. Confirmed active in dynamic analysis.
Page-window bridge reads site state
We observed VibeMate load its page-world script during browsing.
It registers GET_BASE_WINDOW_VALUE: given a dotted path from the content script, the page script walks it from window, calls it if callable, and returns the JSON result.
You visit a page where VibeMate loads its page-world script.
The extension can ask that page script to read a named value from the page window and return it.
| Field | Value | Why it matters | |
|---|---|---|---|
Requested window path | detail.data.key | This tells the page script which value on the page to read for the extension. | |
Function-call flag | detail.data.isFn | This lets the bridge call the selected page value when it is a function, then return the result. | |
Returned page value | GET_BASE_WINDOW_VALUE_RESPONSE.detail.data | This carries the JSON-serialized value back from the page to the extension. |
The content script request and page-world bridge
function q1() {
$ = G1(), X("LVS_SITE", {
currentSite: $
}), window.addEventListener("GET_LVS_CURRENT_SITE", n => {
const a = n.detail?.uuid || "";
X("GET_LVS_CURRENT_SITE_RESPONSE", a ? {
uuid: a,
data: $
} : $)
}), window.addEventListener("GET_BASE_WINDOW_VALUE", n => {
const a = n.detail?.uuid || "",
l = n.detail?.data?.key || "",
r = n.detail?.data?.isFn || !1,
c = l.split(".");
let i = null,
e = "";
if (c.reduce((t, u) => (i = t?.[u], i), window), r && typeof i == "function") try {
e = JSON.stringify(i() || "") || ""
} catch {
e = JSON.stringify(i) || ""
} else e = JSON.stringify(i) || "";
X("GET_BASE_WINDOW_VALUE_RESPONSE", a ? {
uuid: a,
data: e
} : e)
}), $ && (window.addEventListener("SEND_LVS_VIEWER_MESSAGE_BY_WEB_SOCKET", n => {
const {
socketId: a = "",
socketUrl: l = "",
arrToString: r = !1,
params: c = "",
paramsPrefix: i = ""
} = n.detail || {};
a ? Z[a] && (r ? Z[a]?.send(i + c.join(" ")) : Z[a]?.send(i + JSON.stringify(c))) : l && Object.values(Z).forEach(e => {
e.url === l && (r ? e.send(i + c.join(" ")) : e.send(i + JSON.stringify(c)))
})
}), window.addEventListener("SEND_SYNC_LVS_VIEWER_MESSAGE_BY_WEB_SOCKET", n => {
const {
uuid: a = "",
data: l = null
} = n.detail || {}, {
socketId: r = "",
socketUrl: c = "",
arrToString: i = !1,
callbackId: e = "",
params: w = "",
paramsPrefix: t = ""
} = l || {};
let u = {
uuid: a
},
h = null;
if (r ? h = Z[r] : c && Object.values(Z).forEach(g => {
g.url === c && (h = g)
}), h)
if (e) {
const g = p => {
const S = p.detail || {};
if (S.socketId === r) {
const {
data: B = ""
} = S || {};
if (B && F1(B)) {
let s = JSON.parse(B);
s.id === e && (window.removeEventListener("LVS_VIEWER_WEB_SOCKET_MESSAGE", g), X("SEND_SYNC_LVS_VIEWER_MESSAGE_BY_WEB_SOCKET_RESPONSE", {
...u,
data: s
}))
}
}
};
window.addEventListener("LVS_VIEWER_WEB_SOCKET_MESSAGE", g), i ? h.send(t + w.join(" ")) : h.send(t + JSON.stringify(w))
} else i ? h.send(t + w.join(" ")) : h.send(t + JSON.stringify(w)), X("SEND_SYNC_LVS_VIEWER_MESSAGE_BY_WEB_SOCKET_RESPONSE", {
...u,
data: "success"
});
else X("SEND_SYNC_LVS_VIEWER_MESSAGE_BY_WEB_SOCKET_RESPONSE", {
...u,
error: "socket not found"
})
}), window.addEventListener("SEND_LVS_VIEWER_MESSAGE_BY_WORKER", n => {
const {
workerId: a = "",
data: l = ""
} = n.detail || {};
E1[a] && E1[a]?.postMessage(l)
}), J1(window))
}Ar = async (t, e = !1) => {
try {
return await nt("GET_BASE_WINDOW_VALUE", {
key: t,
isFn: e
})
} catch {
return ""
}
}
= "string" ? s = sessionStorage.getItem(r) : s = sessionStorage.getItem(r.key);
break
}
case "GET_WINDOW_VALUE": {
try {
typeof r == "string" ? s = await Ar(r) : s = await Ar(r.key, r.isFn)
} catch {}
break
}
case "GET_DOM_SRC": {
typeof r == "string" ? s = Rr(r) : s = Rr(r.selector);
break
}
case "GET_DOM_TEXT_CONTENT": {- fanberry.com
Allowed by externally_connectable to send extension messages from matching subdomains.
- vibemate.com
Allowed by externally_connectable to send extension messages from matching subdomains.
+1 more finding not shown