Arx / mobile /detect.js
umer-wasim's picture
v0.3: phone check (iPhone, Android) as the Space front page, catalog 0.3 (74 checks), shared vulnerability rules, real-CVE training data
6ccf48e verified
Raw History Blame Contribute Delete
3.98 kB
// What a web page can learn about the phone it runs on. Browsers deliberately hide security settings,
// so this is limited to the OS and version, device model (Android), browser, whether a device unlock
// method is set up, and WebGPU support. Everything else is asked.
function iosVersion(ua) {
// Safari 26+ freezes the OS version in the user agent (it reports 18_6), but its own Version/ follows iOS.
const safari = /Version\/(\d+(?:\.\d+){0,2}).*Safari/.exec(ua);
const os = /OS (\d+)_(\d+)(?:_(\d+))?/.exec(ua);
const osVersion = os ? [os[1], os[2], os[3]].filter(Boolean).join('.') : null;
if (safari && !/CriOS|FxiOS|EdgiOS/.test(ua) && parseInt(safari[1], 10) >= 26) return { version: safari[1], precise: false };
if (osVersion && osVersion !== '18.6') return { version: osVersion, precise: Boolean(os[3]) };
if (safari) return { version: safari[1], precise: false };
return { version: osVersion, precise: false };
}
function browserName(ua, brands) {
let m;
if ((m = /SamsungBrowser\/(\d+(?:\.\d+)?)/.exec(ua))) return `Samsung Internet ${m[1]}`;
if ((m = /EdgA\/(\d+)|EdgiOS\/(\d+)/.exec(ua))) return `Edge ${m[1] || m[2]}`;
if ((m = /Firefox\/(\d+)|FxiOS\/(\d+)/.exec(ua))) return `Firefox ${m[1] || m[2]}`;
if ((m = /CriOS\/(\d+)/.exec(ua))) return `Chrome ${m[1]} (iOS)`;
const chrome = (brands || []).find(b => b.brand === 'Google Chrome');
if (chrome) return `Chrome ${chrome.version}`;
if ((m = /Chrome\/(\d+)/.exec(ua))) return `Chrome ${m[1]}`;
if ((m = /Version\/(\d+(?:\.\d+)?).*Safari/.exec(ua))) return `Safari ${m[1]}`;
return 'Unknown browser';
}
async function withTimeout(promise, ms, fallback) {
return Promise.race([promise, new Promise(resolve => setTimeout(() => resolve(fallback), ms))]);
}
export async function detectDevice() {
const ua = navigator.userAgent;
const uaData = navigator.userAgentData;
const out = { os_family: null, os_name: null, os_version: null, version_precise: false, device_model: '', browser: browserName(ua, uaData && uaData.brands),
platform_authenticator: null, webgpu: 'gpu' in navigator, cellular: Boolean(navigator.connection && navigator.connection.type === 'cellular'), desktop: false };
const ipadDesktopMode = /Macintosh/.test(ua) && navigator.maxTouchPoints > 1;
if (/iPhone|iPod/.test(ua) || /iPad/.test(ua) || ipadDesktopMode) {
const ipad = /iPad/.test(ua) || ipadDesktopMode;
const v = iosVersion(ua);
Object.assign(out, { os_family: 'ios', os_name: ipad ? 'iPadOS' : 'iOS', os_version: v.version, version_precise: v.precise, device_model: ipad ? 'iPad' : 'iPhone' });
} else if (/Android/.test(ua)) {
out.os_family = 'android';
out.os_name = 'Android';
if (uaData && uaData.getHighEntropyValues) {
try {
const hi = await withTimeout(uaData.getHighEntropyValues(['platform', 'platformVersion', 'model']), 1500, {});
// Only trust the hints when they describe Android (desktop browsers emulating a phone report the host OS).
if (hi.platform === 'Android') {
if (hi.platformVersion) out.os_version = String(parseInt(hi.platformVersion, 10));
if (hi.model) out.device_model = hi.model;
}
} catch { /* not allowed */ }
}
if (!out.os_version) {
const m = /Android (\d+)/.exec(ua);
// Chrome's reduced user agent always says "Android 10; K": not a real version.
if (m && !/Android 10; K\)/.test(ua)) out.os_version = m[1];
}
if (!out.device_model) {
const m = /Android [\d.]+; ([^;)]+)[;)]/.exec(ua);
if (m && m[1] !== 'K') out.device_model = m[1].trim();
}
} else {
out.desktop = true;
}
if (window.PublicKeyCredential && PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable) {
try {
out.platform_authenticator = await withTimeout(PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(), 2000, null);
} catch { out.platform_authenticator = null; }
}
return out;
}