@thumbmarkjs/thumbmarkjs 0.19.1 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/thumbmark.cjs.js +1 -1
- package/dist/thumbmark.cjs.js.map +1 -1
- package/dist/thumbmark.esm.d.ts +1 -1
- package/dist/thumbmark.esm.js +1 -1
- package/dist/thumbmark.esm.js.map +1 -1
- package/dist/thumbmark.umd.js +1 -1
- package/dist/thumbmark.umd.js.map +1 -1
- package/package.json +3 -2
- package/src/components/audio/audio.ts +64 -0
- package/src/components/canvas/canvas.test.ts +38 -0
- package/src/components/canvas/canvas.ts +90 -0
- package/src/components/fonts/fonts.ts +145 -0
- package/src/components/hardware/hardware.ts +69 -0
- package/src/components/index.ts +17 -0
- package/src/components/locales/locales.ts +23 -0
- package/src/components/math/math.ts +39 -0
- package/src/components/permissions/permissions.ts +58 -0
- package/src/components/plugins/plugins.ts +22 -0
- package/src/components/screen/screen.ts +46 -0
- package/src/components/system/browser.ts +63 -0
- package/src/components/system/system.ts +40 -0
- package/src/components/webgl/webgl.ts +148 -0
- package/src/declarations.d.ts +15 -0
- package/src/factory.ts +58 -0
- package/src/fingerprint/functions.test.ts +34 -0
- package/src/fingerprint/functions.ts +118 -0
- package/src/fingerprint/options.ts +27 -0
- package/src/index.ts +6 -0
- package/src/utils/commonPixels.ts +38 -0
- package/src/utils/ephemeralIFrame.ts +35 -0
- package/src/utils/getMostFrequent.ts +39 -0
- package/src/utils/hash.ts +198 -0
- package/src/utils/imageDataToDataURL.ts +15 -0
- package/src/utils/raceAll.ts +45 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { componentInterface, includeComponent } from '../../factory'
|
|
2
|
+
|
|
3
|
+
function getHardwareInfo(): Promise<componentInterface> {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const deviceMemory = (navigator.deviceMemory !== undefined) ? navigator.deviceMemory : 0
|
|
6
|
+
const memoryInfo = (window.performance && (window.performance as any).memory ) ? (window.performance as any).memory : 0
|
|
7
|
+
resolve(
|
|
8
|
+
{
|
|
9
|
+
'videocard': getVideoCard(),
|
|
10
|
+
'architecture': getArchitecture(),
|
|
11
|
+
'deviceMemory': deviceMemory.toString() || 'undefined',
|
|
12
|
+
'jsHeapSizeLimit': memoryInfo.jsHeapSizeLimit || 0,
|
|
13
|
+
}
|
|
14
|
+
)
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getVideoCard(): componentInterface | string {
|
|
19
|
+
const canvas = document.createElement('canvas');
|
|
20
|
+
const gl = canvas.getContext('webgl') ?? canvas.getContext('experimental-webgl');
|
|
21
|
+
|
|
22
|
+
if (gl && 'getParameter' in gl) {
|
|
23
|
+
try {
|
|
24
|
+
// Try standard parameters first
|
|
25
|
+
const vendor = (gl.getParameter(gl.VENDOR) || '').toString();
|
|
26
|
+
const renderer = (gl.getParameter(gl.RENDERER) || '').toString();
|
|
27
|
+
|
|
28
|
+
let result: componentInterface = {
|
|
29
|
+
vendor: vendor,
|
|
30
|
+
renderer: renderer,
|
|
31
|
+
version: (gl.getParameter(gl.VERSION) || '').toString(),
|
|
32
|
+
shadingLanguageVersion: (gl.getParameter(gl.SHADING_LANGUAGE_VERSION) || '').toString(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Only try debug info if needed and available
|
|
36
|
+
if (!renderer.length || !vendor.length) {
|
|
37
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
38
|
+
if (debugInfo) {
|
|
39
|
+
const vendorUnmasked = (gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || '').toString();
|
|
40
|
+
const rendererUnmasked = (gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || '').toString();
|
|
41
|
+
|
|
42
|
+
// Only add unmasked values if they exist
|
|
43
|
+
if (vendorUnmasked) {
|
|
44
|
+
result.vendorUnmasked = vendorUnmasked;
|
|
45
|
+
}
|
|
46
|
+
if (rendererUnmasked) {
|
|
47
|
+
result.rendererUnmasked = rendererUnmasked;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// fail silently
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return "undefined";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getArchitecture(): number {
|
|
61
|
+
const f = new Float32Array(1);
|
|
62
|
+
const u8 = new Uint8Array(f.buffer);
|
|
63
|
+
f[0] = Infinity;
|
|
64
|
+
f[0] = f[0] - f[0];
|
|
65
|
+
|
|
66
|
+
return u8[3];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
includeComponent('hardware', getHardwareInfo);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does anyone have a cleaner way of doing this?
|
|
3
|
+
* I want to import all the components in this folder
|
|
4
|
+
* Feels a little dumb I'm doing this manually.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import './audio/audio'
|
|
8
|
+
import './canvas/canvas'
|
|
9
|
+
import './fonts/fonts'
|
|
10
|
+
import './hardware/hardware'
|
|
11
|
+
import './locales/locales'
|
|
12
|
+
import './permissions/permissions'
|
|
13
|
+
import './plugins/plugins'
|
|
14
|
+
import './screen/screen'
|
|
15
|
+
import './system/system'
|
|
16
|
+
import './webgl/webgl'
|
|
17
|
+
import './math/math'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { componentInterface, includeComponent } from '../../factory'
|
|
2
|
+
|
|
3
|
+
function getLocales(): Promise<componentInterface> {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
resolve(
|
|
6
|
+
{
|
|
7
|
+
'languages': getUserLanguage(),
|
|
8
|
+
'timezone': getUserTimezone()
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getUserLanguage(): string {
|
|
14
|
+
const userLanguages: string[] = [];
|
|
15
|
+
|
|
16
|
+
return navigator.language;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getUserTimezone(): string {
|
|
20
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
includeComponent('locales', getLocales);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { componentInterface, includeComponent } from '../../factory'
|
|
2
|
+
|
|
3
|
+
const getMathInfo = async (): Promise<componentInterface> => {
|
|
4
|
+
return {
|
|
5
|
+
acos: Math.acos(0.5),
|
|
6
|
+
asin: integrate(Math.asin, -1, 1, 97),
|
|
7
|
+
atan: integrate(Math.atan, -1, 1, 97),
|
|
8
|
+
cos: integrate(Math.cos, 0, Math.PI, 97),
|
|
9
|
+
cosh: Math.cosh(9/7),
|
|
10
|
+
e: Math.E,
|
|
11
|
+
largeCos: Math.cos(1e20),
|
|
12
|
+
largeSin: Math.sin(1e20),
|
|
13
|
+
largeTan: Math.tan(1e20),
|
|
14
|
+
log: Math.log(1000),
|
|
15
|
+
pi: Math.PI,
|
|
16
|
+
sin: integrate(Math.sin, -Math.PI, Math.PI, 97),
|
|
17
|
+
sinh: integrate(Math.sinh, -9/7, 7/9, 97),
|
|
18
|
+
sqrt: Math.sqrt(2),
|
|
19
|
+
tan: integrate(Math.tan, 0, 2 * Math.PI, 97),
|
|
20
|
+
tanh: integrate(Math.tanh, -9/7, 7/9, 97),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** This might be a little excessive, but I wasn't sure what number to pick for some of the
|
|
25
|
+
* trigonometric functions. Using an integral here, so a few numbers are calculated. However,
|
|
26
|
+
* I do this mainly for those integrals that sum up to a small value, otherwise there's no point.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const integrate = (f: (x: number) => number, a: number, b: number, n: number): number => {
|
|
30
|
+
const h = (b - a) / n;
|
|
31
|
+
let sum = 0;
|
|
32
|
+
for (let i = 0; i < n; i++) {
|
|
33
|
+
const x = a + (i + 0.5) * h;
|
|
34
|
+
sum += f(x);
|
|
35
|
+
}
|
|
36
|
+
return sum * h;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
includeComponent('math', getMathInfo);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { componentInterface, includeComponent } from '../../factory';
|
|
2
|
+
import { mostFrequentValuesInArrayOfDictionaries } from '../../utils/getMostFrequent';
|
|
3
|
+
import { options } from '../../fingerprint/options';
|
|
4
|
+
|
|
5
|
+
let permission_keys: PermissionName[];
|
|
6
|
+
function initializePermissionKeys() {
|
|
7
|
+
permission_keys = options?.permissions_to_check || [
|
|
8
|
+
'accelerometer',
|
|
9
|
+
'accessibility', 'accessibility-events',
|
|
10
|
+
'ambient-light-sensor',
|
|
11
|
+
'background-fetch', 'background-sync', 'bluetooth',
|
|
12
|
+
'camera',
|
|
13
|
+
'clipboard-read',
|
|
14
|
+
'clipboard-write',
|
|
15
|
+
'device-info', 'display-capture',
|
|
16
|
+
'gyroscope', 'geolocation',
|
|
17
|
+
'local-fonts',
|
|
18
|
+
'magnetometer', 'microphone', 'midi',
|
|
19
|
+
'nfc', 'notifications',
|
|
20
|
+
'payment-handler',
|
|
21
|
+
'persistent-storage',
|
|
22
|
+
'push',
|
|
23
|
+
'speaker', 'storage-access',
|
|
24
|
+
'top-level-storage-access',
|
|
25
|
+
'window-management',
|
|
26
|
+
'query',
|
|
27
|
+
] as PermissionName[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default async function getBrowserPermissions(): Promise<componentInterface> {
|
|
31
|
+
initializePermissionKeys();
|
|
32
|
+
const permissionPromises: Promise<componentInterface>[] = Array.from({length: options?.retries || 3}, () => getBrowserPermissionsOnce() );
|
|
33
|
+
return Promise.all(permissionPromises).then((resolvedPermissions) => {
|
|
34
|
+
const permission = mostFrequentValuesInArrayOfDictionaries(resolvedPermissions, permission_keys);
|
|
35
|
+
return permission;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function getBrowserPermissionsOnce(): Promise<componentInterface> {
|
|
40
|
+
|
|
41
|
+
const permissionStatus: { [key: string]: string } = {};
|
|
42
|
+
|
|
43
|
+
for (const feature of permission_keys) {
|
|
44
|
+
try {
|
|
45
|
+
// Request permission status for each feature
|
|
46
|
+
const status = await navigator.permissions.query({ name: feature });
|
|
47
|
+
|
|
48
|
+
// Assign permission status to the object
|
|
49
|
+
permissionStatus[feature] = status.state.toString();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// In case of errors (unsupported features, etc.), do nothing. Not listing them is the same as not supported
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return permissionStatus;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
includeComponent("permissions", getBrowserPermissions);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { componentInterface, includeComponent } from '../../factory'
|
|
2
|
+
|
|
3
|
+
export default function getInstalledPlugins(): Promise<componentInterface> {
|
|
4
|
+
const plugins: string[] = [];
|
|
5
|
+
|
|
6
|
+
if (navigator.plugins) {
|
|
7
|
+
for (let i = 0; i < navigator.plugins.length; i++) {
|
|
8
|
+
const plugin = navigator.plugins[i];
|
|
9
|
+
plugins.push([plugin.name, plugin.filename, plugin.description ].join("|"));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
resolve(
|
|
15
|
+
{
|
|
16
|
+
'plugins': plugins
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
includeComponent('plugins', getInstalledPlugins);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { componentInterface, includeComponent } from '../../factory';
|
|
2
|
+
|
|
3
|
+
function screenDetails(): Promise<componentInterface> {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
resolve(
|
|
6
|
+
{
|
|
7
|
+
'is_touchscreen': navigator.maxTouchPoints > 0,
|
|
8
|
+
'maxTouchPoints': navigator.maxTouchPoints,
|
|
9
|
+
'colorDepth': screen.colorDepth,
|
|
10
|
+
'mediaMatches': matchMedias(),
|
|
11
|
+
}
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function matchMedias(): string[] {
|
|
17
|
+
let results: string[] = [];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const mediaQueries: { [k: string]: string[] } = {
|
|
24
|
+
'prefers-contrast': ['high', 'more', 'low', 'less', 'forced', 'no-preference'],
|
|
25
|
+
'any-hover': ['hover', 'none'],
|
|
26
|
+
'any-pointer': ['none', 'coarse', 'fine'],
|
|
27
|
+
'pointer': ['none', 'coarse', 'fine'],
|
|
28
|
+
'hover': ['hover', 'none'],
|
|
29
|
+
'update': ['fast', 'slow'],
|
|
30
|
+
'inverted-colors': ['inverted', 'none'],
|
|
31
|
+
'prefers-reduced-motion': ['reduce', 'no-preference'],
|
|
32
|
+
'prefers-reduced-transparency': ['reduce', 'no-preference'],
|
|
33
|
+
'scripting': ['none', 'initial-only', 'enabled'],
|
|
34
|
+
'forced-colors': ['active', 'none'],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
Object.keys(mediaQueries).forEach((key) => {
|
|
38
|
+
mediaQueries[key].forEach((value) => {
|
|
39
|
+
if (matchMedia(`(${key}: ${value})`).matches)
|
|
40
|
+
results.push(`${key}: ${value}`);
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
includeComponent('screen', screenDetails);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Define an interface for the browser result
|
|
2
|
+
interface BrowserResult {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Define a function to parse the user agent string and return the browser name and version
|
|
8
|
+
export function getBrowser(): BrowserResult {
|
|
9
|
+
if (typeof navigator === 'undefined') {
|
|
10
|
+
return {
|
|
11
|
+
name: "unknown",
|
|
12
|
+
version: "unknown"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const ua = navigator.userAgent
|
|
16
|
+
// Define some regular expressions to match different browsers and their versions
|
|
17
|
+
const regexes = [
|
|
18
|
+
// Edge
|
|
19
|
+
/(?<name>Edge|Edg)\/(?<version>\d+(?:\.\d+)?)/,
|
|
20
|
+
// Chrome, Chromium, Opera, Vivaldi, Brave, etc.
|
|
21
|
+
/(?<name>(?:Chrome|Chromium|OPR|Opera|Vivaldi|Brave))\/(?<version>\d+(?:\.\d+)?)/,
|
|
22
|
+
// Firefox, Waterfox, etc.
|
|
23
|
+
/(?<name>(?:Firefox|Waterfox|Iceweasel|IceCat))\/(?<version>\d+(?:\.\d+)?)/,
|
|
24
|
+
// Safari, Mobile Safari, etc.
|
|
25
|
+
/(?<name>Safari)\/(?<version>\d+(?:\.\d+)?)/,
|
|
26
|
+
// Internet Explorer, IE Mobile, etc.
|
|
27
|
+
/(?<name>MSIE|Trident|IEMobile).+?(?<version>\d+(?:\.\d+)?)/,
|
|
28
|
+
// Other browsers that use the format "BrowserName/version"
|
|
29
|
+
/(?<name>[A-Za-z]+)\/(?<version>\d+(?:\.\d+)?)/,
|
|
30
|
+
// Samsung internet browser
|
|
31
|
+
/(?<name>SamsungBrowser)\/(?<version>\d+(?:\.\d+)?)/,
|
|
32
|
+
// Samsung browser (Tizen format)
|
|
33
|
+
/(?<name>samsung).*Version\/(?<version>\d+(?:\.\d+)?)/i
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Define a map for browser name translations
|
|
37
|
+
const browserNameMap: { [key: string]: string } = {
|
|
38
|
+
'edg': 'Edge',
|
|
39
|
+
'opr': 'Opera',
|
|
40
|
+
'samsung': 'SamsungBrowser'
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Loop through the regexes and try to find a match
|
|
44
|
+
for (const regex of regexes) {
|
|
45
|
+
const match = ua.match(regex);
|
|
46
|
+
if (match && match.groups) {
|
|
47
|
+
// Translate the browser name if necessary
|
|
48
|
+
const name = browserNameMap[match.groups.name.toLowerCase()] || match.groups.name;
|
|
49
|
+
// Return the browser name and version
|
|
50
|
+
return {
|
|
51
|
+
name: name,
|
|
52
|
+
version: match.groups.version
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// If no match is found, return unknown
|
|
58
|
+
return {
|
|
59
|
+
name: "unknown",
|
|
60
|
+
version: "unknown"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { componentInterface, includeComponent } from '../../factory';
|
|
2
|
+
import { getBrowser } from './browser'
|
|
3
|
+
|
|
4
|
+
function getSystemDetails(): Promise<componentInterface> {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const browser = getBrowser()
|
|
7
|
+
resolve( {
|
|
8
|
+
'platform': window.navigator.platform,
|
|
9
|
+
'cookieEnabled': window.navigator.cookieEnabled,
|
|
10
|
+
'productSub': navigator.productSub,
|
|
11
|
+
'product': navigator.product,
|
|
12
|
+
'useragent': navigator.userAgent,
|
|
13
|
+
'hardwareConcurrency': navigator.hardwareConcurrency,
|
|
14
|
+
'browser': {'name': browser.name, 'version': browser.version },
|
|
15
|
+
'applePayVersion': getApplePayVersion()
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @returns applePayCanMakePayments: boolean, applePayMaxSupportedVersion: number
|
|
22
|
+
*/
|
|
23
|
+
function getApplePayVersion(): number {
|
|
24
|
+
if (window.location.protocol === 'https:' && typeof (window as any).ApplePaySession === 'function') {
|
|
25
|
+
try {
|
|
26
|
+
const versionCheck = (window as any).ApplePaySession.supportsVersion;
|
|
27
|
+
for (let i = 15; i > 0; i--) {
|
|
28
|
+
if (versionCheck(i)) {
|
|
29
|
+
return i;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return 0
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
includeComponent('system', getSystemDetails);
|
|
40
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { componentInterface, includeComponent } from '../../factory'
|
|
2
|
+
import { hash } from '../../utils/hash'
|
|
3
|
+
import { getCommonPixels } from '../../utils/commonPixels';
|
|
4
|
+
import { getBrowser } from '../system/browser';
|
|
5
|
+
|
|
6
|
+
const _RUNS = (getBrowser().name !== 'SamsungBrowser') ? 1 : 3;
|
|
7
|
+
let canvas: HTMLCanvasElement
|
|
8
|
+
let gl: WebGLRenderingContext | null = null;
|
|
9
|
+
|
|
10
|
+
function initializeCanvasAndWebGL() {
|
|
11
|
+
if (typeof document !== 'undefined') {
|
|
12
|
+
canvas = document.createElement('canvas');
|
|
13
|
+
canvas.width = 200;
|
|
14
|
+
canvas.height = 100;
|
|
15
|
+
gl = canvas.getContext('webgl');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function createWebGLFingerprint(): Promise<componentInterface> {
|
|
20
|
+
initializeCanvasAndWebGL();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
|
|
24
|
+
if (!gl) {
|
|
25
|
+
throw new Error('WebGL not supported');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
const imageDatas: ImageData[] = Array.from({length: _RUNS}, () => createWebGLImageData() );
|
|
30
|
+
// and then checking the most common bytes for each channel of each pixel
|
|
31
|
+
const commonImageData = getCommonPixels(imageDatas, canvas.width, canvas.height);
|
|
32
|
+
//const imageData = createWebGLImageData()
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
'commonImageHash': hash(commonImageData.data.toString()).toString(),
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return {
|
|
39
|
+
'webgl': 'unsupported'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createWebGLImageData(): ImageData {
|
|
45
|
+
try {
|
|
46
|
+
|
|
47
|
+
if (!gl) {
|
|
48
|
+
throw new Error('WebGL not supported');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const vertexShaderSource = `
|
|
52
|
+
attribute vec2 position;
|
|
53
|
+
void main() {
|
|
54
|
+
gl_Position = vec4(position, 0.0, 1.0);
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const fragmentShaderSource = `
|
|
59
|
+
precision mediump float;
|
|
60
|
+
void main() {
|
|
61
|
+
gl_FragColor = vec4(0.812, 0.195, 0.553, 0.921); // Set line color
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
66
|
+
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
67
|
+
|
|
68
|
+
if (!vertexShader || !fragmentShader) {
|
|
69
|
+
throw new Error('Failed to create shaders');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
gl.shaderSource(vertexShader, vertexShaderSource);
|
|
73
|
+
gl.shaderSource(fragmentShader, fragmentShaderSource);
|
|
74
|
+
|
|
75
|
+
gl.compileShader(vertexShader);
|
|
76
|
+
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
|
|
77
|
+
throw new Error('Vertex shader compilation failed: ' + gl.getShaderInfoLog(vertexShader));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
gl.compileShader(fragmentShader);
|
|
81
|
+
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
|
|
82
|
+
throw new Error('Fragment shader compilation failed: ' + gl.getShaderInfoLog(fragmentShader));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const shaderProgram = gl.createProgram();
|
|
86
|
+
|
|
87
|
+
if (!shaderProgram) {
|
|
88
|
+
throw new Error('Failed to create shader program');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
gl.attachShader(shaderProgram, vertexShader);
|
|
92
|
+
gl.attachShader(shaderProgram, fragmentShader);
|
|
93
|
+
gl.linkProgram(shaderProgram);
|
|
94
|
+
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
|
|
95
|
+
throw new Error('Shader program linking failed: ' + gl.getProgramInfoLog(shaderProgram));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
gl.useProgram(shaderProgram);
|
|
99
|
+
|
|
100
|
+
// Set up vertices to form lines
|
|
101
|
+
const numSpokes: number = 137;
|
|
102
|
+
const vertices = new Float32Array(numSpokes * 4);
|
|
103
|
+
const angleIncrement = (2 * Math.PI) / numSpokes;
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < numSpokes; i++) {
|
|
106
|
+
const angle = i * angleIncrement;
|
|
107
|
+
|
|
108
|
+
// Define two points for each line (spoke)
|
|
109
|
+
vertices[i * 4] = 0; // Center X
|
|
110
|
+
vertices[i * 4 + 1] = 0; // Center Y
|
|
111
|
+
vertices[i * 4 + 2] = Math.cos(angle) * (canvas.width / 2); // Endpoint X
|
|
112
|
+
vertices[i * 4 + 3] = Math.sin(angle) * (canvas.height / 2); // Endpoint Y
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const vertexBuffer = gl.createBuffer();
|
|
116
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
|
117
|
+
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
|
118
|
+
|
|
119
|
+
const positionAttribute = gl.getAttribLocation(shaderProgram, 'position');
|
|
120
|
+
gl.enableVertexAttribArray(positionAttribute);
|
|
121
|
+
gl.vertexAttribPointer(positionAttribute, 2, gl.FLOAT, false, 0, 0);
|
|
122
|
+
|
|
123
|
+
// Render
|
|
124
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
125
|
+
gl.clearColor(0.0, 0.0, 0.0, 1.0);
|
|
126
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
127
|
+
gl.drawArrays(gl.LINES, 0, numSpokes * 2);
|
|
128
|
+
|
|
129
|
+
const pixelData = new Uint8ClampedArray(canvas.width * canvas.height * 4);
|
|
130
|
+
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixelData);
|
|
131
|
+
const imageData = new ImageData(pixelData, canvas.width, canvas.height);
|
|
132
|
+
|
|
133
|
+
return imageData;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
//console.error(error);
|
|
136
|
+
return new ImageData(1, 1);
|
|
137
|
+
} finally {
|
|
138
|
+
if (gl) {
|
|
139
|
+
// Reset WebGL state
|
|
140
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
141
|
+
gl.useProgram(null);
|
|
142
|
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
143
|
+
gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
includeComponent('webgl', createWebGLFingerprint);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface ApplePaySession {
|
|
2
|
+
new(version: number, paymentRequest: any): ApplePaySession;
|
|
3
|
+
canMakePayments(): boolean;
|
|
4
|
+
supportsVersion(version: number): boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Window {
|
|
8
|
+
webkitAudioContext: typeof AudioContext
|
|
9
|
+
webkitOfflineAudioContext: typeof OfflineAudioContext
|
|
10
|
+
ApplePaySession: typeof ApplePaySession
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Navigator {
|
|
14
|
+
deviceMemory?: number,
|
|
15
|
+
}
|
package/src/factory.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is used to create the includeComponent function as well as the interfaces each of the
|
|
3
|
+
* fingerprint components must implement.
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { options, optionsInterface } from './fingerprint/options';
|
|
8
|
+
|
|
9
|
+
// the component interface is the form of the JSON object the function's promise must return
|
|
10
|
+
export interface componentInterface {
|
|
11
|
+
[key: string]: string | string[] | number | boolean | componentInterface;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// The component function's interface is simply the promise of the above
|
|
16
|
+
export interface componentFunctionInterface {
|
|
17
|
+
(): Promise<componentInterface>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// components include a dictionary of name: function.
|
|
21
|
+
export const components: {[name: string]: componentFunctionInterface} = {};
|
|
22
|
+
|
|
23
|
+
//In case a promise time-outs, this is what we use as the value in place
|
|
24
|
+
export const timeoutInstance: componentInterface = {
|
|
25
|
+
'timeout': "true"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* includeComponent is the function each component function needs to call in order for the component to be included
|
|
30
|
+
* in the fingerprint.
|
|
31
|
+
* @param {string} name - the name identifier of the component
|
|
32
|
+
* @param {componentFunctionInterface} creationFunction - the function that implements the component
|
|
33
|
+
* @returns
|
|
34
|
+
*/
|
|
35
|
+
export const includeComponent = (name:string, creationFunction: componentFunctionInterface) => {
|
|
36
|
+
if (typeof window !== 'undefined')
|
|
37
|
+
components[name] = creationFunction;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The function turns the map of component functions to a map of Promises when called
|
|
42
|
+
* @returns {[name: string]: <Promise>componentInterface}
|
|
43
|
+
*/
|
|
44
|
+
export const getComponentPromises = () => {
|
|
45
|
+
return Object.fromEntries(
|
|
46
|
+
Object.entries(components)
|
|
47
|
+
.filter(([key]) => {
|
|
48
|
+
return !options?.exclude?.includes(key)}
|
|
49
|
+
)
|
|
50
|
+
.filter(([key]) => {
|
|
51
|
+
return options?.include?.some(e => e.includes('.'))
|
|
52
|
+
? options?.include?.some(e => e.startsWith(key))
|
|
53
|
+
: options?.include?.length === 0 || options?.include?.includes(key)
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
.map(([key, value]) => [key, value()])
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {componentInterface} from '../factory'
|
|
2
|
+
import {filterFingerprintData} from './functions'
|
|
3
|
+
|
|
4
|
+
const test_components: componentInterface = {
|
|
5
|
+
'one': '1',
|
|
6
|
+
'two': 2,
|
|
7
|
+
'three': {'a': true, 'b': false}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('component filtering tests', () => {
|
|
11
|
+
test("excluding top level works", () => {
|
|
12
|
+
expect(filterFingerprintData(test_components, ['one'], [])).toMatchObject({
|
|
13
|
+
'two': 2, 'three': {'a': true, 'b': false}
|
|
14
|
+
})
|
|
15
|
+
});
|
|
16
|
+
test("including top level works", () => {
|
|
17
|
+
expect(filterFingerprintData(test_components, [], ['one', 'two'])).toMatchObject({
|
|
18
|
+
'one': '1', 'two': 2
|
|
19
|
+
})
|
|
20
|
+
});
|
|
21
|
+
test("excluding low-level works", () => {
|
|
22
|
+
expect(filterFingerprintData(test_components, ['two', 'three.a'], [])).toMatchObject({
|
|
23
|
+
'one': '1',
|
|
24
|
+
'three': {'b': false}
|
|
25
|
+
})
|
|
26
|
+
});
|
|
27
|
+
test("including low-level works", () => {
|
|
28
|
+
expect(filterFingerprintData(test_components, [], ['one', 'three.b'])).toMatchObject({
|
|
29
|
+
'one': '1',
|
|
30
|
+
'three': {'b': false}
|
|
31
|
+
})
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
});
|