import { attempt, caniuse, captureError } from '../errors' import { lieProps, documentLie, getPluginLies } from '../lies' import { sendToTrash, gibberish } from '../trash' import { hashMini } from '../utils/crypto' import { createTimer, queueEvent, getOS, braveBrowser, decryptUserAgent, getUserAgentPlatform, isUAPostReduction, computeWindowsRelease, logTestResult, performanceLogger, hashSlice, USER_AGENT_OS, PLATFORM_OS, Analysis } from '../utils/helpers' import { HTMLNote, count, modal } from '../utils/html' // special thanks to https://arh.antoinevastel.com for inspiration export default async function getNavigator(workerScope) { try { const timer = createTimer() await queueEvent(timer) let lied = ( lieProps['Navigator.appVersion'] || lieProps['Navigator.deviceMemory'] || lieProps['Navigator.doNotTrack'] || lieProps['Navigator.hardwareConcurrency'] || lieProps['Navigator.language'] || lieProps['Navigator.languages'] || lieProps['Navigator.maxTouchPoints'] || lieProps['Navigator.oscpu'] || lieProps['Navigator.platform'] || lieProps['Navigator.userAgent'] || lieProps['Navigator.vendor'] || lieProps['Navigator.plugins'] || lieProps['Navigator.mimeTypes'] ) || false const credibleUserAgent = ( 'chrome' in window ? navigator.userAgent.includes(navigator.appVersion) : true ) const data = { platform: attempt(() => { const { platform } = navigator const systems = ['win', 'linux', 'mac', 'arm', 'pike', 'linux', 'iphone', 'ipad', 'ipod', 'android', 'x11'] const trusted = typeof platform == 'string' && systems.filter((val) => platform.toLowerCase().includes(val))[0] if (!trusted) { sendToTrash(`platform`, `${platform} is unusual`) } // user agent os lie if (USER_AGENT_OS !== PLATFORM_OS) { lied = true documentLie( `Navigator.platform`, `${PLATFORM_OS} platform and ${USER_AGENT_OS} user agent do not match`, ) } if (platform != workerScope.platform) { lied = true // documented in the worker source } return platform }), system: attempt(() => getOS(navigator.userAgent), 'userAgent system failed'), userAgentParsed: await attempt(async () => { const reportedUserAgent = caniuse(() => navigator.userAgent) const reportedSystem = getOS(reportedUserAgent) const isBrave = await braveBrowser() const report = decryptUserAgent({ ua: reportedUserAgent, os: reportedSystem, isBrave, }) return report }), device: attempt(() => getUserAgentPlatform({ userAgent: navigator.userAgent }), 'userAgent device failed'), userAgent: attempt(() => { const { userAgent } = navigator if (!credibleUserAgent) { sendToTrash('userAgent', `${userAgent} does not match appVersion`) } if (/\s{2,}|^\s|\s$/g.test(userAgent)) { sendToTrash('userAgent', `extra spaces detected`) } const gibbers = gibberish(userAgent) if (!!gibbers.length) { sendToTrash(`userAgent is gibberish`, userAgent) } if (userAgent != workerScope.userAgent) { lied = true // documented in the worker source } return userAgent.trim().replace(/\s{2,}/, ' ') }, 'userAgent failed'), uaPostReduction: isUAPostReduction((navigator || {}).userAgent), appVersion: attempt(() => { const { appVersion } = navigator if (!credibleUserAgent) { sendToTrash('appVersion', `${appVersion} does not match userAgent`) } if ('appVersion' in navigator && !appVersion) { sendToTrash('appVersion', 'Living Standard property returned falsy value') } if (/\s{2,}|^\s|\s$/g.test(appVersion)) { sendToTrash('appVersion', `extra spaces detected`) } return appVersion.trim().replace(/\s{2,}/, ' ') }, 'appVersion failed'), deviceMemory: attempt(() => { if (!('deviceMemory' in navigator)) { return undefined } // @ts-ignore const { deviceMemory } = navigator const trusted = { '0.25': true, '0.5': true, '1': true, '2': true, '4': true, '8': true, } if (!trusted[deviceMemory]) { sendToTrash('deviceMemory', `${deviceMemory} is not a valid value [0.25, 0.5, 1, 2, 4, 8]`) } // @ts-expect-error memory is undefined if not supported const memory = performance?.memory?.jsHeapSizeLimit || null const memoryInGigabytes = memory ? +(memory/1073741824).toFixed(1) : 0 if (memoryInGigabytes > deviceMemory) { sendToTrash('deviceMemory', `available memory ${memoryInGigabytes}GB is greater than device memory ${deviceMemory}GB`) } if (deviceMemory !== workerScope.deviceMemory) { lied = true // documented in the worker source } return deviceMemory }, 'deviceMemory failed'), doNotTrack: attempt(() => { const { doNotTrack } = navigator const trusted = { '1': !0, 'true': !0, 'yes': !0, '0': !0, 'false': !0, 'no': !0, 'unspecified': !0, 'null': !0, 'undefined': !0, } if (!trusted[doNotTrack]) { sendToTrash('doNotTrack - unusual result', doNotTrack) } return doNotTrack }, 'doNotTrack failed'), globalPrivacyControl: attempt(() => { if (!('globalPrivacyControl' in navigator)) { return undefined } // @ts-ignore const { globalPrivacyControl } = navigator const trusted = { '1': !0, 'true': !0, 'yes': !0, '0': !0, 'false': !0, 'no': !0, 'unspecified': !0, 'null': !0, 'undefined': !0, } if (!trusted[globalPrivacyControl]) { sendToTrash('globalPrivacyControl - unusual result', globalPrivacyControl) } return globalPrivacyControl }, 'globalPrivacyControl failed'), hardwareConcurrency: attempt(() => { if (!('hardwareConcurrency' in navigator)) { return undefined } const { hardwareConcurrency } = navigator if (hardwareConcurrency !== workerScope.hardwareConcurrency) { lied = true // documented in the worker source } return hardwareConcurrency }, 'hardwareConcurrency failed'), language: attempt(() => { const { language, languages } = navigator if (language && languages) { // @ts-ignore const lang = /^.{0,2}/g.exec(language)[0] // @ts-ignore const langs = /^.{0,2}/g.exec(languages[0])[0] if (langs != lang) { sendToTrash('language/languages', `${[language, languages].join(' ')} mismatch`) } return `${languages.join(', ')} (${language})` } if (language != workerScope.language) { lied = true documentLie( `Navigator.language`, `${language} does not match worker scope`, ) } if (languages !== workerScope.languages) { lied = true documentLie( `Navigator.languages`, `${languages} does not match worker scope`, ) } return `${language} ${languages}` }, 'language(s) failed'), maxTouchPoints: attempt(() => { if (!('maxTouchPoints' in navigator)) { return null } return navigator.maxTouchPoints }, 'maxTouchPoints failed'), vendor: attempt(() => navigator.vendor, 'vendor failed'), mimeTypes: attempt(() => { const { mimeTypes } = navigator return mimeTypes ? [...mimeTypes].map((m) => m.type) : [] }, 'mimeTypes failed'), // @ts-ignore oscpu: attempt(() => navigator.oscpu, 'oscpu failed'), plugins: attempt(() => { // https://html.spec.whatwg.org/multipage/system-state.html#pdf-viewing-support const { plugins } = navigator if (!(plugins instanceof PluginArray)) { return } const response = plugins ? [...plugins] .map((p) => ({ name: p.name, description: p.description, filename: p.filename, // @ts-ignore version: p.version, })) : [] const { lies } = getPluginLies(plugins, navigator.mimeTypes) if (lies.length) { lied = true lies.forEach((lie) => { return documentLie(`Navigator.plugins`, lie) }) } if (response.length) { response.forEach((plugin) => { const { name, description } = plugin const nameGibbers = gibberish(name) const descriptionGibbers = gibberish(description) if (nameGibbers.length) { sendToTrash(`plugin name is gibberish`, name) } if (descriptionGibbers.length) { sendToTrash(`plugin description is gibberish`, description) } return }) } return response }, 'plugins failed'), properties: attempt(() => { const keys = Object.keys(Object.getPrototypeOf(navigator)) return keys }, 'navigator keys failed'), } const getUserAgentData = () => attempt(() => { // @ts-ignore if (!navigator.userAgentData || // @ts-ignore !navigator.userAgentData.getHighEntropyValues) { return } // @ts-ignore return navigator.userAgentData.getHighEntropyValues( ['platform', 'platformVersion', 'architecture', 'bitness', 'model', 'uaFullVersion'], ).then((data) => { // @ts-ignore const { brands, mobile } = navigator.userAgentData || {} const compressedBrands = (brands, captureVersion = false) => brands .filter((obj) => !/Not/.test(obj.brand)).map((obj) => `${obj.brand}${captureVersion ? ` ${obj.version}` : ''}`) const removeChromium = (brands) => ( brands.length > 1 ? brands.filter((brand) => !/Chromium/.test(brand)) : brands ) // compress brands if (!data.brands) { data.brands = brands } data.brandsVersion = compressedBrands(data.brands, true) data.brands = compressedBrands(data.brands) data.brandsVersion = removeChromium(data.brandsVersion) data.brands = removeChromium(data.brands) if (!data.mobile) { data.mobile = mobile } const dataSorted = Object.keys(data).sort().reduce((acc, key) => { acc[key] = data[key] return acc }, {}) return dataSorted }) }, 'userAgentData failed') const getBluetoothAvailability = () => attempt(() => { if ( !('bluetooth' in navigator) || // @ts-ignore !navigator.bluetooth || // @ts-ignore !navigator.bluetooth.getAvailability) { return undefined } // @ts-ignore return navigator.bluetooth.getAvailability() }, 'bluetoothAvailability failed') const getPermissions = () => attempt(() => { const getPermissionState = (name) => navigator.permissions.query({ name }) .then((res) => ({ name, state: res.state })) .catch((error) => ({ name, state: 'unknown' })) // https://w3c.github.io/permissions/#permission-registry const permissions = !('permissions' in navigator) ? undefined : Promise.all([ getPermissionState('accelerometer'), getPermissionState('ambient-light-sensor'), getPermissionState('background-fetch'), getPermissionState('background-sync'), getPermissionState('bluetooth'), getPermissionState('camera'), getPermissionState('clipboard'), getPermissionState('device-info'), getPermissionState('display-capture'), getPermissionState('gamepad'), getPermissionState('geolocation'), getPermissionState('gyroscope'), getPermissionState('magnetometer'), getPermissionState('microphone'), getPermissionState('midi'), getPermissionState('nfc'), getPermissionState('notifications'), getPermissionState('persistent-storage'), getPermissionState('push'), getPermissionState('screen-wake-lock'), getPermissionState('speaker'), getPermissionState('speaker-selection'), ]).then((permissions) => permissions.reduce((acc, perm) => { const { state, name } = perm || {} if (acc[state]) { acc[state].push(name) return acc } acc[state] = [name] return acc }, {})).catch((error) => console.error(error)) return permissions }, 'permissions failed') const getWebGpu = () => attempt(() => { if (!('gpu' in navigator)) { return } // @ts-expect-error if unsupported return navigator.gpu.requestAdapter().then((adapter) => { if (!adapter) return const { limits = {}, features = [] } = adapter || {} // @ts-expect-error if unsupported return adapter.requestAdapterInfo().then((info) => { const { architecture, description, device, vendor } = info const adapterInfo = [vendor, architecture, description, device] const featureValues = [...features.values()] const limitsData = ((limits) => { const data: Record = {} // eslint-disable-next-line guard-for-in for (const prop in limits) { data[prop] = limits[prop] } return data })(limits) Analysis.webGpuAdapter = adapterInfo Analysis.webGpuFeatures = hashMini(featureValues) Analysis.webGpuLimits = hashMini(limitsData) return { adapterInfo, features: featureValues, limits: limitsData, } }) }) }, 'webgpu failed') await queueEvent(timer) return Promise.all([ getUserAgentData(), getBluetoothAvailability(), getPermissions(), getWebGpu(), ]).then(([ userAgentData, bluetoothAvailability, permissions, webgpu, ]) => { logTestResult({ time: timer.stop(), test: 'navigator', passed: true }) return { ...data, userAgentData, bluetoothAvailability, permissions, webgpu, lied, } }).catch((error) => { console.error(error) logTestResult({ time: timer.stop(), test: 'navigator', passed: true }) return { ...data, lied, } }) } catch (error) { logTestResult({ test: 'navigator', passed: false }) captureError(error, 'Navigator failed or blocked by client') return } } export function navigatorHTML(fp) { if (!fp.navigator) { return `
Navigator
properties (0): ${HTMLNote.BLOCKED}
dnt: ${HTMLNote.BLOCKED}
gpc:${HTMLNote.BLOCKED}
lang: ${HTMLNote.BLOCKED}
mimeTypes (0): ${HTMLNote.BLOCKED}
permissions (0): ${HTMLNote.BLOCKED}
plugins (0): ${HTMLNote.BLOCKED}
vendor: ${HTMLNote.BLOCKED}
webgpu: ${HTMLNote.BLOCKED}
userAgentData:
${HTMLNote.BLOCKED}
device:
${HTMLNote.BLOCKED}
ua parsed: ${HTMLNote.BLOCKED}
userAgent:
${HTMLNote.BLOCKED}
appVersion:
${HTMLNote.BLOCKED}
` } const { navigator: { $hash, appVersion, deviceMemory, doNotTrack, globalPrivacyControl, hardwareConcurrency, language, maxTouchPoints, mimeTypes, oscpu, permissions, platform, plugins, properties, system, device, userAgent, uaPostReduction, userAgentData, userAgentParsed, vendor, bluetoothAvailability, webgpu, lied, }, } = fp const id = 'creep-navigator' const blocked = { ['null']: !0, ['undefined']: !0, ['']: !0, } const permissionsKeys = Object.keys(permissions || {}) const permissionsGranted = ( permissions && permissions.granted ? permissions.granted.length : 0 ) return ` ${performanceLogger.getLog().navigator}
Navigator${hashSlice($hash)}
properties (${count(properties)}): ${ modal( `${id}-properties`, properties.join(', '), hashMini(properties), ) }
dnt: ${'' + doNotTrack}
gpc: ${ '' + globalPrivacyControl == 'undefined' ? HTMLNote.UNSUPPORTED : '' + globalPrivacyControl }
lang: ${ !blocked[language] ? language : HTMLNote.BLOCKED }
mimeTypes (${count(mimeTypes)}): ${ !blocked['' + mimeTypes] ? modal( `${id}-mimeTypes`, mimeTypes.join('
'), hashMini(mimeTypes), ) : HTMLNote.BLOCKED }
permissions (${''+permissionsGranted}): ${ !permissions || !permissionsKeys ? HTMLNote.UNSUPPORTED : modal( 'creep-permissions', permissionsKeys.map((key) => `
${key}:
${permissions[key].join('
')}
`).join(''), hashMini(permissions), ) }
plugins (${count(plugins)}): ${ !blocked['' + plugins] ? modal( `${id}-plugins`, plugins.map((plugin) => plugin.name).join('
'), hashMini(plugins), ) : HTMLNote.BLOCKED }
vendor: ${!blocked[vendor] ? vendor : HTMLNote.BLOCKED}
webgpu: ${!webgpu ? HTMLNote.UNSUPPORTED : modal( `${id}-webgpu`, ((webgpu) => { const { adapterInfo, features, limits } = webgpu return `
Adapter
${adapterInfo.filter((x: string) => x).join('
')}

Features
${features.join('
')}

Limits
${Object.keys(limits).map((x) => `${x}: ${limits[x]}`).join('
')}
` })(webgpu), hashMini(webgpu), ) }
userAgentData:
${((userAgentData) => { const { architecture, bitness, brandsVersion, uaFullVersion, mobile, model, platformVersion, platform, } = userAgentData || {} // @ts-ignore const windowsRelease = computeWindowsRelease({ platform, platformVersion }) return !userAgentData ? HTMLNote.UNSUPPORTED : ` ${(brandsVersion || []).join(',')}${uaFullVersion ? ` (${uaFullVersion})` : ''}
${windowsRelease || `${platform} ${platformVersion}`} ${architecture ? `${architecture}${bitness ? `_${bitness}` : ''}` : ''} ${model ? `
${model}` : ''} ${mobile ? '
mobile' : ''} ` })(userAgentData)}
device:
${oscpu ? oscpu : ''} ${`${oscpu ? '
' : ''}${system}${platform ? ` (${platform})` : ''}`} ${device ? `
${device}` : HTMLNote.BLOCKED}${ hardwareConcurrency && deviceMemory ? `
cores: ${hardwareConcurrency}, ram: ${deviceMemory}` : hardwareConcurrency && !deviceMemory ? `
cores: ${hardwareConcurrency}` : !hardwareConcurrency && deviceMemory ? `
ram: ${deviceMemory}` : '' }${typeof maxTouchPoints != 'undefined' ? `, touch: ${''+maxTouchPoints}` : ''}${bluetoothAvailability ? `, bluetooth` : ''}
ua parsed: ${userAgentParsed || HTMLNote.BLOCKED}
userAgent:${!uaPostReduction ? '' : `ua reduction`}
${userAgent || HTMLNote.BLOCKED}
appVersion:
${appVersion || HTMLNote.BLOCKED}
` }