Is Manga Translator馃崜 safe?
Manga Translator sends a device fingerprint and base64-encoded image data to ichigoreader.com with every translation request.
When a user triggers image translation, the extension captures the target image as base64 and transmits it to ichigoreader.com/translate along with a detailed hardware fingerprint. The fingerprint includes WebGL renderer info, canvas hash, screen dimensions, hardware concurrency, device memory, connection type, locale, and other browser capability flags. Both the fingerprint and image data are sent on every translation call, not just at install time.
Who publishes itIchigo LLC - no other listings under this identity, 2 shared hostnames
Ichigo LLC - no other listings under this identity, 2 shared hostnames
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 2 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.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Manga Translator sends a device fingerprint
Translating an image builds a fingerprint from your renderer, screen, hardware, locale, capabilities, touch/orientation, user-agent, and JS heap signals, sent to ichigoreader.com/translate and also as a query parameter to /metrics.
What actually happens
You enable translation on a page or request an image translation.
The content script then finds eligible page images and converts them into translation jobs.
The extension computes a device fingerprint and attaches it to translation traffic.
The same fingerprint function is referenced by the translation POST and the metrics GET.
Fields attached to translation traffic
- Device fingerprint3f2a91c0b8a4d12e7c903f1a5e61b02a9d4c8f33
Lets the service correlate translation requests from the same browser even when the page being translated changes.
- Client ID9b3b3a8e-7d52-4e19-9b8c-91c6f9a2d4b7
Gives the service another stable identifier for the same extension install.
- Image being translateddata:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=
Sends the selected page image to the translation service so text can be extracted and translated.
- Target languageen
Shows which translation language you requested for the image.
- Network address198.51.100.23 (illustrative)
The server receiving the request can associate the fingerprint with the connection used for translation.
Captured request
Confirmed during dynamic analysis: the POST body contained a fingerprint field. The retained evidence did not include a full body copy.
The code that does this
Fingerprint creation and request attachment in the shipped background bundle
Readable fingerprint builder
deobfuscated/js/background.jst.getFingerprint = function() { if (d) return d; const e = function() { try { if (s) { const e = s.createElement("canvas").getContext("webgl"); if (!e) return "no-webgl"; const t = e.getExtension("WEBGL_debug_renderer_info"); return t ? e.getParameter(t.UNMASKED_RENDERER_WEBGL) : "unknown" } if ("undefined" != typeof OffscreenCanvas) { const e = new OffscreenCanvas(1, 1).getContext("webgl"); if (!e) return "no-webgl"; const t = e.getExtension("WEBGL_debug_renderer_info"); return t ? e.getParameter(t.UNMASKED_RENDERER_WEBGL) : "unknown" } } catch (e) { return "webgl-error" } return "webgl-unavailable" }(), t = `${null!==(w=null==l?void 0:l.hardwareConcurrency)&&void 0!==w?w:"unknown"}-${null!==(b=null==l?void 0:l.deviceMemory)&&void 0!==b?b:"unknown"}`, n = function() { var e, t, n, o, r; const i = null == l ? void 0 : l.connection; return `${null!==(e=null==i?void 0:i.type)&&void 0!==e?e:"unknown"}-${null!==(t=null==i?void 0:i.rtt)&&void 0!==t?t:"unknown"}-${null!==(n=null==i?void 0:i.downlinkMax)&&void 0!==n?n:"unknown"}-${null!==(o=null==i?void 0:i.effectiveType)&&void 0!==o?o:"unknown"}-${null!==(r=null==i?void 0:i.saveData)&&void 0!==r&&r}` }(), o = (new Date).getTimezoneOffset(), r = function() { var e; const t = u; if (!t) return "screen-unavailable"; return `${t.width}x${t.height}-${t.colorDepth}-${t.pixelDepth}-${t.availWidth}x${t.availHeight}-${null!==(e=null==a?void 0:a.devicePixelRatio)&&void 0!==e?e:1}` }(), f = function() { try { if (!s) return "canvas-unavailable"; const e = s.createElement("canvas"), t = e.getContext("2d"); if (!t) return "no-2d-context"; t.textBaseline = "top", t.font = "14px Arial", t.fillStyle = "rgba(102, 204, 0, 0.7)", t.fillRect(125, 1, 62, 20), t.fillStyle = "#f60", t.fillText("BrowserLeaks,com <canvas> 1.0", 2, 15), t.fillStyle = "rgba(102, 204, 0, 0.7)", t.fillText("BrowserLeaks,com <canvas> 1.0", 4, 17); const n = e.toDataURL(); let o = 0; for (let e = 0; e < n.length; e++) o = (o << 5) - o + n.charCodeAt(e), o |= 0; return o.toString() } catch (e) { return "canvas-error" } }(), g = function() { const e = [], t = l, n = a; return t && "serviceWorker" in t && e.push("sw"), a && "localStorage" in a && e.push("ls"), a && "sessionStorage" in a && e.push("ss"), "indexedDB" in i && e.push("idb"), t && "geolocation" in t && e.push("geo"), "Notification" in i && e.push("notif"), t && "permissions" in t && e.push("perm"), "boolean" == typeof(null == t ? void 0 : t.cookieEnabled) && t.cookieEnabled && e.push("cookie"), void 0 !== (null == t ? void 0 : t.doNotTrack) && e.push("dnt"), t && "onLine" in t && e.push("online"), t && "deviceMemory" in t && e.push("devmem"), t && "connection" in t && e.push("conn"), "function" == typeof(null == t ? void 0 : t.getBattery) && e.push("bat"), t && "mediaDevices" in t && e.push("media"), n && "webkitRequestFileSystem" in n && e.push("fs"), n && "webkitStorageInfo" in n && e.push("storage"), e.sort().join(",") }(), v = function() { var e, t, n, o; const r = (null == l ? void 0 : l.languages) && l.languages.length > 0 ? l.languages : (null == l ? void 0 : l.language) ? [l.language] : []; return `${null!==(e=null==l?void 0:l.language)&&void 0!==e?e:"unknown"}-${null!==(t=null==l?void 0:l.userLanguage)&&void 0!==t?t:"unknown"}-${null!==(n=null==l?void 0:l.browserLanguage)&&void 0!==n?n:"unknown"}-${null!==(o=null==l?void 0:l.systemLanguage)&&void 0!==o?o:"unknown"}-${r.join(",")}` }(), m = function() { var e; const t = a; return `${null!==(e=null==l?void 0:l.maxTouchPoints)&&void 0!==e?e:0}-${!(!t||!("ontouchstart"in t))}-${"TouchEvent"in i}-${"PointerEvent"in i}` }(), h = function() { var e, t, n, o, r; const i = a; return `${null!==(t=null===(e=null==u?void 0:u.orientation)||void 0===e?void 0:e.type)&&void 0!==t?t:"unknown"}-${null!==(r=null!==(o=null===(n=null==u?void 0:u.orientation)||void 0===n?void 0:n.angle)&&void 0!==o?o:null==i?void 0:i.orientation)&&void 0!==r?r:0}-${!(!a||!("onorientationchange"in a))}` }(), p = function() { var e, t, n, o, r; const i = null !== (e = null == l ? void 0 : l.userAgent) && void 0 !== e ? e : "unknown"; return `${null!==(t=null==l?void 0:l.platform)&&void 0!==t?t:"unknown"}-${null!==(n=null==l?void 0:l.vendor)&&void 0!==n?n:""}-${!!(null==l?void 0:l.cookieEnabled)}-${"function"==typeof(null==l?void 0:l.javaEnabled)&&l.javaEnabled()}-${/iPad|iPhone|iPod/.test(i)}-${/Safari/.test(i)&&!/Chrome/.test(i)}-${(null===(o=i.match(/Version\/(\d+)/))||void 0===o?void 0:o[1])||"unknown"}-${(null===(r=i.match(/(iPhone|iPad|iPod)/))||void 0===r?void 0:r[1])||"unknown"}` }(), y = [e, t, n, o, r, f, g, v, m, h, p, function() { try { const e = null != c ? c : null == a ? void 0 : a.performance; if (!e) return "no-performance"; const t = e.timing, n = e.memory, o = null == l ? void 0 : l.connection, r = (null == t ? void 0 : t.navigationStart) || 0, i = ((null == t ? void 0 : t.loadEventEnd) || 0) - r, s = (null == n ? void 0 : n.usedJSHeapSize) || 0, u = (null == n ? void 0 : n.totalJSHeapSize) || 0, d = (null == n ? void 0 : n.jsHeapSizeLimit) || 0, f = (null == o ? void 0 : o.effectiveType) || "unknown"; return `${i}-${s}-${u}-${d}-${f}-${(null==o?void 0:o.downlink)||0}-${(null==o?void 0:o.rtt)||0}` } catch (e) { return "performance-error" } }()].join("-"); var w, b; return d = function(e) { let t = 5381; for (let n = 0; n < e.length; n++) t = 33 * t ^ e.charCodeAt(n); let n = (t >>> 0).toString(16).padStart(8, "0"); const o = Math.ceil(e.length / 4); for (let t = 0; t < 4; t++) { const r = e.slice(t * o, (t + 1) * o); let i = 5381; for (let e = 0; e < r.length; e++) i = 33 * i ^ r.charCodeAt(e); n += (i >>> 0).toString(16).padStart(8, "0") } return n }(y), d }Readable metrics request
deobfuscated/js/background.jst.getCurrentUser = function() { return o(this, void 0, void 0, (function*() { yield u(); const e = yield r.appConfig.getClientUuid(), n = f(), o = yield fetch(`${t.baseUrl}/metrics?clientUuid=${e}&fingerprint=${(0,i.getFingerprint)()}`, { method: "GET", headers: n }); if (o.status !== s.Ok) throw new Error("Failed to retrieve user."); return yield o.json() })) }Readable translation request
deobfuscated/js/background.jst.translateImage = function(e, n, a) { return o(this, void 0, void 0, (function*() { const o = yield r.appConfig.getClientUuid(), l = yield fetch(`${t.baseUrl}/translate`, { method: "POST", headers: f(), body: JSON.stringify({ base64Images: [n], translationModel: a, targetLangCode: e, fingerprint: (0, i.getFingerprint)(), clientUuid: o }) }); if (l.status === s.InternalServerError) { const e = "Server is down or experiencing issues. Sorry for the inconvenience."; return { errorMessage: e, translations: [{ originalLanguage: "Unknown", translatedText: e, minX: 0, minY: 0, maxX: 200, maxY: 200 }] } } if (l.status === s.TooManyRequests) { const e = "Out of translations. Server costs are expensive. Upgrade for more!"; return { errorMessage: e, translations: [{ originalLanguage: "Unknown", translatedText: e, minX: 0, minY: 0, maxX: 200, maxY: 200 }] } } return { translations: (yield l.json()).images[0] } })) }Hosts receiving the fingerprint
- ichigoreader.com
Receives translation POSTs at /translate with fingerprint and clientUuid fields; the code also sends the fingerprint in the /metrics query string.
Where it sends data
Destinations our analysis observed manga-translator%F0%9F%8D%93 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.
- ichigoreader.com
manga-translator%F0%9F%8D%93 sends data to ichigoreader.com. No other extension we have analyzed sends data here.
