├── .env.example ├── server └── tsconfig.json ├── app ├── components │ ├── ui │ │ ├── progress │ │ │ ├── index.ts │ │ │ └── Progress.vue │ │ ├── skeleton │ │ │ ├── index.ts │ │ │ └── Skeleton.vue │ │ ├── separator │ │ │ ├── index.ts │ │ │ └── Separator.vue │ │ ├── scroll-area │ │ │ ├── index.ts │ │ │ ├── ScrollArea.vue │ │ │ └── ScrollBar.vue │ │ ├── tooltip │ │ │ ├── index.ts │ │ │ ├── TooltipTrigger.vue │ │ │ ├── TooltipProvider.vue │ │ │ ├── Tooltip.vue │ │ │ └── TooltipContent.vue │ │ ├── dialog │ │ │ ├── DialogClose.vue │ │ │ ├── DialogTrigger.vue │ │ │ ├── DialogHeader.vue │ │ │ ├── Dialog.vue │ │ │ ├── DialogFooter.vue │ │ │ ├── index.ts │ │ │ ├── DialogDescription.vue │ │ │ ├── DialogTitle.vue │ │ │ ├── DialogScrollContent.vue │ │ │ └── DialogContent.vue │ │ ├── card │ │ │ ├── CardContent.vue │ │ │ ├── CardFooter.vue │ │ │ ├── CardHeader.vue │ │ │ ├── index.ts │ │ │ ├── CardDescription.vue │ │ │ ├── CardTitle.vue │ │ │ └── Card.vue │ │ ├── alert │ │ │ ├── AlertDescription.vue │ │ │ ├── AlertTitle.vue │ │ │ ├── Alert.vue │ │ │ └── index.ts │ │ ├── badge │ │ │ ├── Badge.vue │ │ │ └── index.ts │ │ ├── button │ │ │ ├── Button.vue │ │ │ └── index.ts │ │ ├── chart │ │ │ ├── index.ts │ │ │ ├── ChartTooltip.vue │ │ │ ├── ChartCrosshair.vue │ │ │ ├── interface.ts │ │ │ ├── ChartLegend.vue │ │ │ └── ChartSingleTooltip.vue │ │ └── chart-line │ │ │ ├── index.ts │ │ │ └── LineChart.vue │ ├── AudioVisualizer.vue │ ├── FingerprintIcon.vue │ ├── FingerprintInfoModal.vue │ └── StatsBar.vue ├── lib │ └── utils.ts ├── app.vue ├── composables │ ├── useHardwareInfo.ts │ ├── useCanvasFingerprint.ts │ ├── useFingerprintHelpers.ts │ ├── useWebGLFingerprint.ts │ ├── useAudioFingerprint.ts │ ├── useFontDetection.ts │ ├── useSystemInfo.ts │ ├── useBrowserFingerprint.ts │ ├── useIncognitoDetection.ts │ └── useEntropyCalculator.ts ├── assets │ └── css │ │ └── tailwind.css └── pages │ ├── links.vue │ ├── privacy-tools.vue │ └── index.vue ├── public ├── favicon.ico ├── _robots.txt └── fingerprint.png ├── tsconfig.json ├── .gitignore ├── components.json ├── .github └── FUNDING.yml ├── LICENSE ├── package.json ├── content ├── fingerprinting │ ├── webgl.md │ ├── screen.md │ ├── hardware.md │ ├── browser.md │ ├── media.md │ └── system.md ├── privacy-tools.md └── links.md ├── nuxt.config.ts ├── tailwind.config.js ├── .cursorrules ├── README.md └── design.md /.env.example: -------------------------------------------------------------------------------- 1 | UMAMI_WEBSITE_ID=your-website-id 2 | UMAMI_HOST=your-umami-host -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /app/components/ui/progress/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Progress } from './Progress.vue' 2 | -------------------------------------------------------------------------------- /app/components/ui/skeleton/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Skeleton } from './Skeleton.vue' 2 | -------------------------------------------------------------------------------- /app/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Separator } from './Separator.vue' 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonKohli/browser-fingerprint/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/_robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | # Sitemap 5 | Sitemap: https://trackme.dev/sitemap.xml -------------------------------------------------------------------------------- /public/fingerprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonKohli/browser-fingerprint/HEAD/public/fingerprint.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /app/components/ui/scroll-area/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ScrollArea } from './ScrollArea.vue' 2 | export { default as ScrollBar } from './ScrollBar.vue' 3 | -------------------------------------------------------------------------------- /app/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /app/components/ui/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tooltip } from './Tooltip.vue' 2 | export { default as TooltipContent } from './TooltipContent.vue' 3 | export { default as TooltipProvider } from './TooltipProvider.vue' 4 | export { default as TooltipTrigger } from './TooltipTrigger.vue' 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /app/components/ui/dialog/DialogClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /app/components/ui/dialog/DialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /app/components/ui/tooltip/TooltipTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /app/components/ui/tooltip/TooltipProvider.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /app/components/ui/card/CardContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/components/ui/card/CardFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/components/ui/card/CardHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Card } from './Card.vue' 2 | export { default as CardContent } from './CardContent.vue' 3 | export { default as CardDescription } from './CardDescription.vue' 4 | export { default as CardFooter } from './CardFooter.vue' 5 | export { default as CardHeader } from './CardHeader.vue' 6 | export { default as CardTitle } from './CardTitle.vue' 7 | -------------------------------------------------------------------------------- /app/components/ui/card/CardDescription.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/components/ui/alert/AlertDescription.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/components/ui/alert/AlertTitle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/components/ui/skeleton/Skeleton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /app/components/ui/dialog/DialogHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /app/components/ui/card/CardTitle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /app/app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /app/components/ui/dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/components/ui/tooltip/Tooltip.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/components/ui/badge/Badge.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /app/components/ui/card/Card.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /app/components/ui/alert/Alert.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /app/components/ui/dialog/DialogFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-vue.com/schema.json", 3 | "style": "default", 4 | "typescript": true, 5 | "tsConfigPath": ".nuxt/tsconfig.json", 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/assets/css/tailwind.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "framework": "nuxt", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils" 17 | } 18 | } -------------------------------------------------------------------------------- /app/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Dialog } from './Dialog.vue' 2 | export { default as DialogClose } from './DialogClose.vue' 3 | export { default as DialogContent } from './DialogContent.vue' 4 | export { default as DialogDescription } from './DialogDescription.vue' 5 | export { default as DialogFooter } from './DialogFooter.vue' 6 | export { default as DialogHeader } from './DialogHeader.vue' 7 | export { default as DialogScrollContent } from './DialogScrollContent.vue' 8 | export { default as DialogTitle } from './DialogTitle.vue' 9 | export { default as DialogTrigger } from './DialogTrigger.vue' 10 | -------------------------------------------------------------------------------- /app/components/ui/button/Button.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /app/components/ui/dialog/DialogDescription.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /app/components/ui/dialog/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /app/components/ui/chart/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ChartCrosshair } from './ChartCrosshair.vue' 2 | export { default as ChartLegend } from './ChartLegend.vue' 3 | export { default as ChartSingleTooltip } from './ChartSingleTooltip.vue' 4 | export { default as ChartTooltip } from './ChartTooltip.vue' 5 | 6 | export function defaultColors(count: number = 3) { 7 | const quotient = Math.floor(count / 2) 8 | const remainder = count % 2 9 | 10 | const primaryCount = quotient + remainder 11 | const secondaryCount = quotient 12 | return [ 13 | ...Array.from(new Array(primaryCount).keys()).map(i => `hsl(var(--vis-primary-color) / ${1 - (1 / primaryCount) * i})`), 14 | ...Array.from(new Array(secondaryCount).keys()).map(i => `hsl(var(--vis-secondary-color) / ${1 - (1 / secondaryCount) * i})`), 15 | ]; 16 | } 17 | 18 | export * from './interface' 19 | -------------------------------------------------------------------------------- /app/components/ui/alert/index.ts: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | 3 | export { default as Alert } from './Alert.vue' 4 | export { default as AlertDescription } from './AlertDescription.vue' 5 | export { default as AlertTitle } from './AlertTitle.vue' 6 | 7 | export const alertVariants = cva( 8 | 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-background text-foreground', 13 | destructive: 14 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', 15 | }, 16 | }, 17 | defaultVariants: { 18 | variant: 'default', 19 | }, 20 | }, 21 | ) 22 | 23 | export type AlertVariants = VariantProps 24 | -------------------------------------------------------------------------------- /app/components/ui/scroll-area/ScrollArea.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | -------------------------------------------------------------------------------- /app/components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | 3 | export { default as Badge } from './Badge.vue' 4 | 5 | export const badgeVariants = cva( 6 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 7 | { 8 | variants: { 9 | variant: { 10 | default: 11 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', 12 | secondary: 13 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 14 | destructive: 15 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 16 | outline: 'text-foreground', 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: 'default', 21 | }, 22 | }, 23 | ) 24 | 25 | export type BadgeVariants = VariantProps 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: leonkohli # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: leonkohli # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: leonkohli # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /app/components/ui/progress/Progress.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 40 | -------------------------------------------------------------------------------- /app/components/ui/scroll-area/ScrollBar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Leon Kohli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/components/ui/separator/Separator.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare", 11 | "start": "nuxt start" 12 | }, 13 | "dependencies": { 14 | "@nuxt/content": "^2.13.4", 15 | "@nuxt/icon": "^1.10.3", 16 | "@nuxtjs/color-mode": "^3.5.2", 17 | "@nuxtjs/seo": "^2.1.1", 18 | "@nuxtjs/tailwindcss": "^6.13.1", 19 | "@unovis/ts": "^1.5.0", 20 | "@unovis/vue": "^1.5.0", 21 | "@vueuse/core": "^11.3.0", 22 | "@vueuse/nuxt": "^11.3.0", 23 | "class-variance-authority": "^0.7.1", 24 | "clsx": "^2.1.1", 25 | "lucide-vue-next": "^0.455.0", 26 | "nuxt": "^3.15.4", 27 | "nuxt-umami": "^3.1.1", 28 | "radix-vue": "^1.9.13", 29 | "shadcn-nuxt": "^0.11.3", 30 | "tailwind-merge": "^2.6.0", 31 | "tailwindcss-animate": "^1.0.7", 32 | "vue": "latest", 33 | "vue-router": "latest" 34 | }, 35 | "devDependencies": { 36 | "@iconify-json/mdi": "^1.2.3", 37 | "typescript": "^5.7.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/components/ui/chart/ChartTooltip.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 41 | -------------------------------------------------------------------------------- /app/composables/useHardwareInfo.ts: -------------------------------------------------------------------------------- 1 | export function useHardwareInfo() { 2 | const getHardwareAcceleration = () => { 3 | if (!import.meta.client) return false; 4 | 5 | const canvas = document.createElement('canvas'); 6 | const ctx = canvas.getContext('webgl'); 7 | if (!ctx) return false; 8 | 9 | const debugInfo = ctx.getExtension('WEBGL_debug_renderer_info'); 10 | if (!debugInfo) return false; 11 | 12 | const renderer = ctx.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); 13 | return !/SwiftShader|Software|Microsoft Basic Render/i.test(renderer); 14 | }; 15 | 16 | const getTouchSupport = () => { 17 | if (!import.meta.client) return null; 18 | 19 | return { 20 | maxTouchPoints: navigator.maxTouchPoints, 21 | touchEvent: 'ontouchstart' in window, 22 | touchPoints: navigator.maxTouchPoints || 0 23 | }; 24 | }; 25 | 26 | const getColorDepth = () => { 27 | if (!import.meta.client) return null; 28 | 29 | return { 30 | colorDepth: screen.colorDepth, 31 | pixelDepth: screen.pixelDepth 32 | }; 33 | }; 34 | 35 | return { 36 | getHardwareAcceleration, 37 | getTouchSupport, 38 | getColorDepth 39 | }; 40 | } -------------------------------------------------------------------------------- /app/components/ui/tooltip/TooltipContent.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 32 | -------------------------------------------------------------------------------- /app/composables/useCanvasFingerprint.ts: -------------------------------------------------------------------------------- 1 | export function useCanvasFingerprint() { 2 | const generateCanvasFingerprint = (): string | null => { 3 | if (!import.meta.client) return null; 4 | 5 | try { 6 | const canvas = document.createElement('canvas'); 7 | canvas.width = 200; 8 | canvas.height = 200; 9 | const ctx = canvas.getContext('2d'); 10 | 11 | if (!ctx) { 12 | return null; 13 | } 14 | 15 | ctx.textBaseline = 'top'; 16 | ctx.font = '14px Arial'; 17 | ctx.textBaseline = 'alphabetic'; 18 | ctx.fillStyle = '#f60'; 19 | ctx.fillRect(125, 1, 62, 20); 20 | ctx.fillStyle = '#069'; 21 | ctx.fillText('Browser Fingerprint', 2, 15); 22 | ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'; 23 | ctx.fillText('Canvas Test', 4, 45); 24 | 25 | ctx.strokeStyle = 'rgba(102, 204, 0, 0.7)'; 26 | ctx.beginPath(); 27 | ctx.arc(50, 50, 30, 0, Math.PI * 2); 28 | ctx.stroke(); 29 | 30 | return canvas.toDataURL(); 31 | } catch (error) { 32 | console.error('Error generating canvas fingerprint:', error); 33 | return null; 34 | } 35 | }; 36 | 37 | return { 38 | generateCanvasFingerprint 39 | }; 40 | } -------------------------------------------------------------------------------- /app/composables/useFingerprintHelpers.ts: -------------------------------------------------------------------------------- 1 | export function useFingerprintHelpers() { 2 | const formatKey = (key: string): string => { 3 | return key 4 | .replace(/([A-Z])/g, ' $1') 5 | .replace(/^./, str => str.toUpperCase()) 6 | .trim(); 7 | }; 8 | 9 | const copyFingerprint = async (hash: string | undefined): Promise => { 10 | if (!process.client || !hash) return; 11 | 12 | try { 13 | await navigator.clipboard.writeText(hash); 14 | // Handle success (e.g., show a notification) 15 | } catch (error) { 16 | console.error('Failed to copy:', error); 17 | } 18 | }; 19 | 20 | const getSectionDescription = (sectionId: string): string => { 21 | const descriptions: Record = { 22 | browser: 'Information about your web browser and its capabilities', 23 | system: 'Details about your operating system and environment', 24 | screen: 'Display and resolution information', 25 | media: 'Audio and visual capabilities', 26 | hardware: 'Device hardware specifications', 27 | webgl: 'Graphics and rendering capabilities' 28 | }; 29 | return descriptions[sectionId] || ''; 30 | }; 31 | 32 | return { 33 | formatKey, 34 | copyFingerprint, 35 | getSectionDescription 36 | }; 37 | } -------------------------------------------------------------------------------- /app/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | 3 | export { default as Button } from './Button.vue' 4 | 5 | export const buttonVariants = cva( 6 | 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', 7 | { 8 | variants: { 9 | variant: { 10 | default: 11 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90', 12 | destructive: 13 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', 14 | outline: 15 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', 16 | secondary: 17 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', 18 | ghost: 'hover:bg-accent hover:text-accent-foreground', 19 | link: 'text-primary underline-offset-4 hover:underline', 20 | }, 21 | size: { 22 | default: 'h-9 px-4 py-2', 23 | sm: 'h-8 rounded-md px-3 text-xs', 24 | lg: 'h-10 rounded-md px-8', 25 | icon: 'h-9 w-9', 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: 'default', 30 | size: 'default', 31 | }, 32 | }, 33 | ) 34 | 35 | export type ButtonVariants = VariantProps 36 | -------------------------------------------------------------------------------- /app/components/ui/chart/ChartCrosshair.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 45 | -------------------------------------------------------------------------------- /app/components/ui/chart/interface.ts: -------------------------------------------------------------------------------- 1 | import type { Spacing } from '@unovis/ts' 2 | 3 | type KeyOf> = Extract 4 | 5 | export interface BaseChartProps> { 6 | /** 7 | * The source data, in which each entry is a dictionary. 8 | */ 9 | data: T[] 10 | /** 11 | * Select the categories from your data. Used to populate the legend and toolip. 12 | */ 13 | categories: KeyOf[] 14 | /** 15 | * Sets the key to map the data to the axis. 16 | */ 17 | index: KeyOf 18 | /** 19 | * Change the default colors. 20 | */ 21 | colors?: string[] 22 | /** 23 | * Margin of each the container 24 | */ 25 | margin?: Spacing 26 | /** 27 | * Change the opacity of the non-selected field 28 | * @default 0.2 29 | */ 30 | filterOpacity?: number 31 | /** 32 | * Function to format X label 33 | */ 34 | xFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string 35 | /** 36 | * Function to format Y label 37 | */ 38 | yFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string 39 | /** 40 | * Controls the visibility of the X axis. 41 | * @default true 42 | */ 43 | showXAxis?: boolean 44 | /** 45 | * Controls the visibility of the Y axis. 46 | * @default true 47 | */ 48 | showYAxis?: boolean 49 | /** 50 | * Controls the visibility of tooltip. 51 | * @default true 52 | */ 53 | showTooltip?: boolean 54 | /** 55 | * Controls the visibility of legend. 56 | * @default true 57 | */ 58 | showLegend?: boolean 59 | /** 60 | * Controls the visibility of gridline. 61 | * @default true 62 | */ 63 | showGridLine?: boolean 64 | } 65 | -------------------------------------------------------------------------------- /app/components/ui/chart-line/index.ts: -------------------------------------------------------------------------------- 1 | export { default as LineChart } from './LineChart.vue' 2 | 3 | import type { Spacing } from '@unovis/ts' 4 | 5 | type KeyOf> = Extract 6 | 7 | export interface BaseChartProps> { 8 | /** 9 | * The source data, in which each entry is a dictionary. 10 | */ 11 | data: T[] 12 | /** 13 | * Select the categories from your data. Used to populate the legend and toolip. 14 | */ 15 | categories: KeyOf[] 16 | /** 17 | * Sets the key to map the data to the axis. 18 | */ 19 | index: KeyOf 20 | /** 21 | * Change the default colors. 22 | */ 23 | colors?: string[] 24 | /** 25 | * Margin of each the container 26 | */ 27 | margin?: Spacing 28 | /** 29 | * Change the opacity of the non-selected field 30 | * @default 0.2 31 | */ 32 | filterOpacity?: number 33 | /** 34 | * Function to format X label 35 | */ 36 | xFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string 37 | /** 38 | * Function to format Y label 39 | */ 40 | yFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string 41 | /** 42 | * Controls the visibility of the X axis. 43 | * @default true 44 | */ 45 | showXAxis?: boolean 46 | /** 47 | * Controls the visibility of the Y axis. 48 | * @default true 49 | */ 50 | showYAxis?: boolean 51 | /** 52 | * Controls the visibility of tooltip. 53 | * @default true 54 | */ 55 | showTooltip?: boolean 56 | /** 57 | * Controls the visibility of legend. 58 | * @default true 59 | */ 60 | showLegend?: boolean 61 | /** 62 | * Controls the visibility of gridline. 63 | * @default true 64 | */ 65 | showGridLine?: boolean 66 | } 67 | -------------------------------------------------------------------------------- /app/assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --muted: 240 4.8% 95.9%; 11 | --muted-foreground: 240 3.8% 46.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 240 10% 3.9%; 18 | 19 | --border: 240 5.9% 90%; 20 | --input: 240 5.9% 90%; 21 | 22 | --primary: 240 5.9% 10%; 23 | --primary-foreground: 0 0% 98%; 24 | 25 | --secondary: 240 4.8% 95.9%; 26 | --secondary-foreground: 240 5.9% 10%; 27 | 28 | --accent: 240 4.8% 95.9%; 29 | --accent-foreground: 240 5.9% 10%; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | 34 | --ring: 240 10% 3.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 240 10% 3.9%; 41 | --foreground: 0 0% 98%; 42 | 43 | --muted: 240 3.7% 15.9%; 44 | --muted-foreground: 240 5% 64.9%; 45 | 46 | --popover: 240 10% 3.9%; 47 | --popover-foreground: 0 0% 98%; 48 | 49 | --card: 240 10% 3.9%; 50 | --card-foreground: 0 0% 98%; 51 | 52 | --border: 240 3.7% 15.9%; 53 | --input: 240 3.7% 15.9%; 54 | 55 | --primary: 0 0% 98%; 56 | --primary-foreground: 240 5.9% 10%; 57 | 58 | --secondary: 240 3.7% 15.9%; 59 | --secondary-foreground: 0 0% 98%; 60 | 61 | --accent: 240 3.7% 15.9%; 62 | --accent-foreground: 0 0% 98%; 63 | 64 | --destructive: 0 62.8% 30.6%; 65 | --destructive-foreground: 0 0% 98%; 66 | 67 | --ring: 240 4.9% 83.9%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } -------------------------------------------------------------------------------- /app/components/ui/chart/ChartLegend.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 51 | -------------------------------------------------------------------------------- /content/fingerprinting/webgl.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: WebGL Information 3 | description: Graphics rendering capabilities and characteristics of your device's GPU. 4 | icon: mdi:video-3d 5 | sections: 6 | - title: How it Works 7 | icon: mdi:code-braces 8 | content: > 9 | WebGL fingerprinting in this implementation collects information about your graphics hardware using the WebGL API. It retrieves the renderer and vendor strings through the WEBGL_debug_renderer_info extension, along with supported WebGL extensions and various rendering parameters like maximum texture size, viewport dimensions, and other GPU capabilities. 10 | 11 | - title: Privacy Implications 12 | icon: mdi:shield-alert 13 | content: > 14 | WebGL information can uniquely identify your graphics hardware and drivers, making it a powerful component of device fingerprinting. This data is particularly stable as it's tied to your physical hardware, and changes only when you upgrade your GPU or drivers. 15 | 16 | - title: Protection Strategies 17 | icon: mdi:shield-check 18 | items: 19 | - Use browsers that limit or standardize WebGL information exposure. 20 | - Consider using WebGL blocking extensions, but be aware this may break some websites. 21 | - Keep your graphics drivers updated to maintain common configurations. 22 | - Use virtual machines or common hardware configurations when privacy is crucial. 23 | - Consider using privacy-focused browsers that provide WebGL fingerprinting protection. 24 | 25 | - title: Additional Resources 26 | icon: mdi:book-open-page-variant 27 | resources: 28 | - label: WebGL API Documentation 29 | url: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API 30 | icon: mdi:code-tags 31 | - label: WebGL Security Considerations 32 | url: https://www.khronos.org/webgl/security/ 33 | icon: mdi:shield 34 | --- 35 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | compatibilityDate: '2024-04-03', 4 | devtools: { enabled: true }, 5 | modules: ['@nuxtjs/tailwindcss', 'shadcn-nuxt', '@nuxtjs/color-mode', '@nuxt/icon', '@vueuse/nuxt', '@nuxtjs/seo', '@nuxt/content', 'nuxt-umami'], 6 | future: { 7 | compatibilityVersion: 4, 8 | }, 9 | shadcn: { 10 | prefix: '', 11 | componentDir: './app/components/ui' 12 | }, 13 | colorMode: { 14 | classPrefix: '', 15 | classSuffix: '', 16 | }, 17 | site: { 18 | url: 'https://trackme.dev/', 19 | name: 'Browser Fingerprint', 20 | description: 'Generate and analyze your unique browser fingerprint. Understand how websites can track you.', 21 | defaultLocale: 'en', 22 | }, 23 | ogImage: { 24 | // Enable dynamic OG image generation 25 | enabled: true, 26 | defaults: { 27 | component: 'NuxtSeo', 28 | width: 1200, 29 | height: 630, 30 | }, 31 | }, 32 | runtimeConfig: { 33 | public: { 34 | umamiWebsiteId: process.env.UMAMI_WEBSITE_ID, 35 | umamiHost: process.env.UMAMI_HOST, 36 | } 37 | }, 38 | content: { 39 | highlight: { 40 | theme: 'github-dark', 41 | }, 42 | }, 43 | 44 | umami: { 45 | id: process.env.UMAMI_WEBSITE_ID, 46 | host: process.env.UMAMI_HOST, 47 | autoTrack: true, 48 | ignoreLocalhost: true, 49 | useDirective: true, 50 | // proxy: 'cloak', 51 | // excludeQueryParams: false, 52 | // domains: ['cool-site.app', 'my-space.site'], 53 | // customEndpoint: '/my-custom-endpoint', 54 | // enabled: false, 55 | // logErrors: true, 56 | }, 57 | 58 | app: { 59 | head: { 60 | script: [ 61 | { 62 | src: 'https://webry.leonkohli.de/api/script.js', 63 | defer: true, 64 | 'data-site-id': '1', 65 | }, 66 | ], 67 | }, 68 | }, 69 | }) -------------------------------------------------------------------------------- /content/fingerprinting/screen.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Screen Information 3 | description: Details about your device's display properties and configurations. 4 | icon: mdi:monitor 5 | sections: 6 | - title: How it Works 7 | icon: mdi:code-braces 8 | content: > 9 | Screen fingerprinting in this implementation collects basic display characteristics using JavaScript's Screen API. This includes screen width and height, color depth, device pixel ratio (which indicates display scaling), and screen orientation. These properties are accessed directly through the window.screen object and related browser APIs. 10 | 11 | - title: Privacy Implications 12 | icon: mdi:shield-alert 13 | content: > 14 | Screen properties can be surprisingly revealing about your device type and setup. Unusual screen resolutions, non-standard pixel ratios, or specific orientations can make your device more identifiable. This information, combined with other fingerprinting data, helps create a more unique profile of your device. 15 | 16 | - title: Protection Strategies 17 | icon: mdi:shield-check 18 | items: 19 | - Use common screen resolutions and standard scaling settings when possible. 20 | - Consider using browser extensions that can spoof screen properties. 21 | - Be aware that changing display settings might affect your device's fingerprint. 22 | - Use privacy-focused browsers that standardize or limit screen information exposure. 23 | - Consider the impact of multi-monitor setups on your fingerprint uniqueness. 24 | 25 | - title: Additional Resources 26 | icon: mdi:book-open-page-variant 27 | resources: 28 | - label: Screen API Documentation 29 | url: https://developer.mozilla.org/en-US/docs/Web/API/Screen 30 | icon: mdi:code-tags 31 | - label: Window.devicePixelRatio Documentation 32 | url: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio 33 | icon: mdi:monitor 34 | --- 35 | -------------------------------------------------------------------------------- /content/fingerprinting/hardware.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hardware Information 3 | description: Physical characteristics and capabilities of your device. 4 | icon: mdi:memory 5 | sections: 6 | - title: How it Works 7 | icon: mdi:code-braces 8 | content: > 9 | Hardware fingerprinting in this implementation collects information about your device's physical capabilities using various browser APIs. This includes the number of CPU cores (via hardwareConcurrency), device memory, maximum touch points, and the availability of various hardware features like battery status, bluetooth, gamepads, and wake lock capabilities. 10 | 11 | - title: Privacy Implications 12 | icon: mdi:shield-alert 13 | content: > 14 | Hardware characteristics provide a stable and reliable way to identify devices, as they rarely change unless you upgrade your hardware. The combination of CPU cores, memory, and hardware feature support creates a distinctive profile that can be used to track your device across different browsers and sessions. 15 | 16 | - title: Protection Strategies 17 | icon: mdi:shield-check 18 | items: 19 | - Use privacy-focused browsers that limit hardware information exposure. 20 | - Consider using virtual machines with standardized hardware configurations. 21 | - Be aware that hardware-level fingerprinting is difficult to prevent without impacting functionality. 22 | - Keep your hardware configurations as common as possible to blend in with other users. 23 | - Use browser extensions that can spoof or limit hardware information access. 24 | 25 | - title: Additional Resources 26 | icon: mdi:book-open-page-variant 27 | resources: 28 | - label: Navigator.hardwareConcurrency 29 | url: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/hardwareConcurrency 30 | icon: mdi:cpu-64-bit 31 | - label: Device Memory API 32 | url: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/deviceMemory 33 | icon: mdi:memory 34 | --- 35 | -------------------------------------------------------------------------------- /app/components/ui/dialog/DialogScrollContent.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 60 | -------------------------------------------------------------------------------- /content/fingerprinting/browser.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Browser Information 3 | description: Detailed insights into your web browser and its capabilities. 4 | icon: mdi:web 5 | sections: 6 | - title: How it Works 7 | icon: mdi:code-braces 8 | content: > 9 | Browser fingerprinting in this implementation collects various browser-specific attributes using JavaScript APIs. This includes the user agent string, browser language settings, cookie enablement status, Do Not Track preference, vendor information, and installed plugins. It also detects whether you're using private browsing mode and identifies your specific browser name and version through user agent analysis. 10 | 11 | - title: Privacy Implications 12 | icon: mdi:shield-alert 13 | content: > 14 | The combination of browser attributes creates a distinctive profile that can be used to identify and track your online activity. Even when using privacy features like VPNs or clearing cookies, these browser characteristics remain consistent and can be used to recognize your device across different websites and sessions. 15 | 16 | - title: Protection Strategies 17 | icon: mdi:shield-check 18 | items: 19 | - Use privacy-focused browsers like [Tor Browser](https://www.torproject.org/) or [Brave](https://brave.com/) that standardize browser fingerprints. 20 | - Enable privacy and tracking protection features in your browser settings. 21 | - Consider using browser extensions that modify or spoof browser information. 22 | - Regularly update your browser to benefit from the latest privacy enhancements. 23 | - Be cautious about installing browser plugins as they can make your fingerprint more unique. 24 | 25 | - title: Additional Resources 26 | icon: mdi:book-open-page-variant 27 | resources: 28 | - label: Navigator API Documentation 29 | url: https://developer.mozilla.org/en-US/docs/Web/API/Navigator 30 | icon: mdi:code-tags 31 | - label: User Agent Documentation 32 | url: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent 33 | icon: mdi:web 34 | --- 35 | -------------------------------------------------------------------------------- /app/components/ui/dialog/DialogContent.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 51 | -------------------------------------------------------------------------------- /app/components/ui/chart/ChartSingleTooltip.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 64 | -------------------------------------------------------------------------------- /content/fingerprinting/media.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Media Capabilities 3 | description: Analysis of your device's audio and visual processing features. 4 | icon: mdi:harddisk 5 | sections: 6 | - title: How it Works 7 | icon: mdi:code-braces 8 | content: > 9 | Media fingerprinting in this implementation focuses on two main aspects: Canvas and Audio fingerprinting. Canvas fingerprinting works by drawing specific text, shapes, and colors using the HTML5 Canvas element and converting the result to a data URL. The subtle differences in how devices render these elements create a unique fingerprint. Audio fingerprinting uses the Web Audio API to create and process audio signals through an oscillator and compressor, analyzing the resulting audio data to generate a unique hash based on how your device processes audio. 10 | 11 | - title: Privacy Implications 12 | icon: mdi:shield-alert 13 | content: > 14 | Canvas and Audio fingerprinting are particularly powerful tracking methods because they capture hardware-level differences in how your device processes media. These techniques work silently in the background and can identify your device even when using private browsing or clearing cookies. The fingerprints generated are highly reliable as they depend on your device's hardware and software configuration, which typically doesn't change frequently. 15 | 16 | - title: Protection Strategies 17 | icon: mdi:shield-check 18 | items: 19 | - Use browsers with built-in Canvas/Audio fingerprinting protection like Tor Browser or Brave. 20 | - Install extensions like [CanvasBlocker](https://addons.mozilla.org/en-US/firefox/addon/canvasblocker/) to prevent or spoof canvas fingerprinting. 21 | - Consider using script-blocking extensions like [uBlock Origin](https://github.com/gorhill/uBlock) to control which sites can access Canvas and Audio APIs. 22 | - Keep your browser updated to benefit from the latest privacy protections. 23 | - Be aware that blocking these APIs completely may break some website functionality. 24 | 25 | - title: Additional Resources 26 | icon: mdi:book-open-page-variant 27 | resources: 28 | - label: Canvas API Documentation 29 | url: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API 30 | icon: mdi:code-tags 31 | - label: Web Audio API Documentation 32 | url: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API 33 | icon: mdi:volume-high 34 | --- 35 | -------------------------------------------------------------------------------- /content/fingerprinting/system.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: System Information 3 | description: Details about your operating system and environmental settings. 4 | icon: mdi:cpu-64-bit 5 | sections: 6 | - title: How it Works 7 | icon: mdi:code-braces 8 | content: > 9 | System fingerprinting in this implementation collects information about your system's environment using JavaScript APIs. This includes your timezone settings (obtained via Intl.DateTimeFormat), timezone offset (from Date.getTimezoneOffset()), and a comprehensive list of installed system fonts. The font detection is performed by measuring text rendering differences across various font families using the Canvas API. 10 | 11 | - title: Privacy Implications 12 | icon: mdi:shield-alert 13 | content: > 14 | System information can reveal significant details about your device setup. Your timezone settings are determined by your system's date/time configuration, not your IP address or VPN. This means using a VPN alone won't mask your timezone information. Installed fonts can reveal information about your work (design software, specialized applications) or language preferences. The combination of these attributes contributes to creating a unique identifier for your device. 15 | 16 | - title: Protection Strategies 17 | icon: mdi:shield-check 18 | items: 19 | # Timezone-specific strategies 20 | - "Timezone Protection: Override JavaScript's Date and Intl APIs using extensions like Chameleon or User-Agent Switcher" 21 | - "System Time: Ensure your system time matches your spoofed timezone to prevent inconsistencies" 22 | # Font-specific strategies 23 | - "Font Protection: Use Font Fingerprint Defender or similar extensions to limit font enumeration" 24 | - "Font Management: Stick to system default fonts and remove unnecessary custom fonts" 25 | - "Font Installation: Create a separate user profile for design work requiring custom fonts" 26 | # Specific to our implementation 27 | - "Implementation Specific: Our tool detects fonts using canvas measurements - consider using Canvas Blocker extensions" 28 | 29 | - title: Additional Resources 30 | icon: mdi:book-open-page-variant 31 | resources: 32 | - label: JavaScript Date.getTimezoneOffset() Documentation 33 | url: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset 34 | icon: mdi:clock 35 | - label: Intl.DateTimeFormat Documentation 36 | url: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat 37 | icon: mdi:clock 38 | - label: Font Fingerprinting Technical Details 39 | url: https://browserleaks.com/fonts 40 | icon: mdi:format-font 41 | --- 42 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const animate = require("tailwindcss-animate") 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | darkMode: ["class"], 6 | safelist: ["dark"], 7 | prefix: "", 8 | 9 | theme: { 10 | container: { 11 | center: true, 12 | padding: "2rem", 13 | screens: { 14 | "2xl": "1400px", 15 | }, 16 | }, 17 | extend: { 18 | colors: { 19 | border: "hsl(var(--border))", 20 | input: "hsl(var(--input))", 21 | ring: "hsl(var(--ring))", 22 | background: "hsl(var(--background))", 23 | foreground: "hsl(var(--foreground))", 24 | primary: { 25 | DEFAULT: "hsl(var(--primary))", 26 | foreground: "hsl(var(--primary-foreground))", 27 | }, 28 | secondary: { 29 | DEFAULT: "hsl(var(--secondary))", 30 | foreground: "hsl(var(--secondary-foreground))", 31 | }, 32 | destructive: { 33 | DEFAULT: "hsl(var(--destructive))", 34 | foreground: "hsl(var(--destructive-foreground))", 35 | }, 36 | muted: { 37 | DEFAULT: "hsl(var(--muted))", 38 | foreground: "hsl(var(--muted-foreground))", 39 | }, 40 | accent: { 41 | DEFAULT: "hsl(var(--accent))", 42 | foreground: "hsl(var(--accent-foreground))", 43 | }, 44 | popover: { 45 | DEFAULT: "hsl(var(--popover))", 46 | foreground: "hsl(var(--popover-foreground))", 47 | }, 48 | card: { 49 | DEFAULT: "hsl(var(--card))", 50 | foreground: "hsl(var(--card-foreground))", 51 | }, 52 | }, 53 | borderRadius: { 54 | xl: "calc(var(--radius) + 4px)", 55 | lg: "var(--radius)", 56 | md: "calc(var(--radius) - 2px)", 57 | sm: "calc(var(--radius) - 4px)", 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | "collapsible-down": { 69 | from: { height: 0 }, 70 | to: { height: 'var(--radix-collapsible-content-height)' }, 71 | }, 72 | "collapsible-up": { 73 | from: { height: 'var(--radix-collapsible-content-height)' }, 74 | to: { height: 0 }, 75 | }, 76 | }, 77 | animation: { 78 | "accordion-down": "accordion-down 0.2s ease-out", 79 | "accordion-up": "accordion-up 0.2s ease-out", 80 | "collapsible-down": "collapsible-down 0.2s ease-in-out", 81 | "collapsible-up": "collapsible-up 0.2s ease-in-out", 82 | }, 83 | }, 84 | }, 85 | plugins: [animate], 86 | } -------------------------------------------------------------------------------- /app/composables/useWebGLFingerprint.ts: -------------------------------------------------------------------------------- 1 | export function useWebGLFingerprint() { 2 | const isWebGLSupported = useSupported(() => { 3 | const canvas = document.createElement('canvas') 4 | return !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl')) 5 | }) 6 | 7 | const getWebGLFingerprint = () => { 8 | if (!import.meta.client || !isWebGLSupported.value) { 9 | return { 10 | error: 'WebGL not supported', 11 | renderer: null, 12 | vendor: null 13 | } 14 | } 15 | 16 | try { 17 | const canvas = document.createElement('canvas'); 18 | const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); 19 | 20 | if (!gl) { 21 | return { 22 | error: 'WebGL not supported', 23 | renderer: null, 24 | vendor: null 25 | }; 26 | } 27 | 28 | const glContext = gl as WebGLRenderingContext; 29 | const debugInfo = glContext.getExtension('WEBGL_debug_renderer_info'); 30 | 31 | if (!debugInfo) { 32 | return { 33 | error: 'WEBGL_debug_renderer_info not supported', 34 | renderer: glContext.getParameter(glContext.RENDERER), 35 | vendor: glContext.getParameter(glContext.VENDOR) 36 | }; 37 | } 38 | 39 | return { 40 | renderer: glContext.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL), 41 | vendor: glContext.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL), 42 | extensions: glContext.getSupportedExtensions(), 43 | parameters: { 44 | maxTextureSize: glContext.getParameter(glContext.MAX_TEXTURE_SIZE), 45 | maxViewportDims: glContext.getParameter(glContext.MAX_VIEWPORT_DIMS), 46 | maxRenderbufferSize: glContext.getParameter(glContext.MAX_RENDERBUFFER_SIZE), 47 | aliasedLineWidthRange: glContext.getParameter(glContext.ALIASED_LINE_WIDTH_RANGE), 48 | aliasedPointSizeRange: glContext.getParameter(glContext.ALIASED_POINT_SIZE_RANGE), 49 | maxVertexAttribs: glContext.getParameter(glContext.MAX_VERTEX_ATTRIBS), 50 | maxVaryingVectors: glContext.getParameter(glContext.MAX_VARYING_VECTORS), 51 | maxVertexUniformVectors: glContext.getParameter(glContext.MAX_VERTEX_UNIFORM_VECTORS), 52 | maxFragmentUniformVectors: glContext.getParameter(glContext.MAX_FRAGMENT_UNIFORM_VECTORS) 53 | } 54 | }; 55 | } catch (error) { 56 | return { 57 | error: 'Error getting WebGL information', 58 | renderer: null, 59 | vendor: null 60 | }; 61 | } 62 | }; 63 | 64 | return { 65 | getWebGLFingerprint, 66 | isWebGLSupported 67 | }; 68 | } -------------------------------------------------------------------------------- /app/composables/useAudioFingerprint.ts: -------------------------------------------------------------------------------- 1 | interface AudioFingerprintData { 2 | hash: number; 3 | } 4 | 5 | export function useAudioFingerprint() { 6 | const isSupported = useSupported(() => window.OfflineAudioContext !== undefined) 7 | 8 | const getAudioFingerprint = async (): Promise => { 9 | if (!isSupported.value) return null; 10 | 11 | return new Promise((resolve) => { 12 | const { start, stop } = useTimeoutFn(() => { 13 | console.warn('Audio fingerprint generation timed out') 14 | resolve(null) 15 | }, 5000) 16 | 17 | start() 18 | 19 | const generateFingerprint = async () => { 20 | try { 21 | // Get appropriate AudioContext constructor 22 | const AudioContextClass = window.OfflineAudioContext; 23 | if (!AudioContextClass) { 24 | return null; 25 | } 26 | 27 | // Create audio context 28 | const context = new AudioContextClass(1, 5000, 44100); 29 | 30 | // Create oscillator 31 | const oscillator = context.createOscillator(); 32 | oscillator.type = "triangle"; 33 | oscillator.frequency.value = 1000; 34 | 35 | // Create compressor 36 | const compressor = context.createDynamicsCompressor(); 37 | compressor.threshold.value = -50; 38 | compressor.knee.value = 40; 39 | compressor.ratio.value = 12; 40 | compressor.attack.value = 0; 41 | compressor.release.value = 0.2; 42 | 43 | // Connect nodes 44 | oscillator.connect(compressor); 45 | compressor.connect(context.destination); 46 | 47 | // Start oscillator 48 | oscillator.start(); 49 | 50 | // Render audio and calculate hash 51 | const buffer = await new Promise((resolve) => { 52 | context.oncomplete = (event) => resolve(event.renderedBuffer); 53 | context.startRendering(); 54 | }); 55 | 56 | // Get samples and calculate hash 57 | const samples = buffer.getChannelData(0); 58 | let hash = 0; 59 | for (let i = 0; i < samples.length; ++i) { 60 | const sample = samples[i]; 61 | if (typeof sample === 'number') { 62 | hash += Math.abs(sample); 63 | } 64 | } 65 | 66 | return { hash }; 67 | } catch (error) { 68 | console.error('Error generating audio fingerprint:', error); 69 | return null; 70 | } 71 | }; 72 | 73 | generateFingerprint().then((result) => { 74 | stop(); 75 | resolve(result); 76 | }); 77 | }); 78 | }; 79 | 80 | return { 81 | getAudioFingerprint, 82 | isSupported 83 | }; 84 | } -------------------------------------------------------------------------------- /app/composables/useFontDetection.ts: -------------------------------------------------------------------------------- 1 | export function useFontDetection() { 2 | const getFonts = () => { 3 | if (!import.meta.client) return { fonts: [], total: 0 }; 4 | 5 | const baseFonts = ['monospace', 'sans-serif', 'serif']; 6 | const fontList = [ 7 | // Common Windows Fonts 8 | 'Arial', 'Arial Black', 'Arial Narrow', 'Calibri', 'Cambria', 9 | 'Cambria Math', 'Comic Sans MS', 'Courier', 'Courier New', 10 | 'Georgia', 'Helvetica', 'Impact', 'Times', 'Times New Roman', 11 | 'Trebuchet MS', 'Verdana', 'Segoe UI', 'Tahoma', 'Consolas', 12 | 'Lucida Console', 'MS Gothic', 'MS PGothic', 'MS Sans Serif', 13 | 'MS Serif', 'Palatino Linotype', 'Book Antiqua', 14 | 15 | // Common Mac Fonts 16 | 'American Typewriter', 'Andale Mono', 'Apple Chancery', 17 | 'Apple Color Emoji', 'Apple SD Gothic Neo', 'AppleGothic', 18 | 'Avenir', 'Avenir Next', 'Baskerville', 'Big Caslon', 19 | 'Brush Script MT', 'Chalkboard', 'Cochin', 'Copperplate', 20 | 'Didot', 'Futura', 'Geneva', 'Gill Sans', 'Helvetica Neue', 21 | 'Herculanum', 'Hoefler Text', 'Lucida Grande', 'Luminari', 22 | 'Marker Felt', 'Menlo', 'Monaco', 'Noteworthy', 'Optima', 23 | 'Papyrus', 'Phosphate', 'Rockwell', 'Skia', 'Snell Roundhand', 24 | 'Zapfino', 25 | 26 | // Common Linux Fonts 27 | 'DejaVu Sans', 'DejaVu Sans Mono', 'DejaVu Serif', 'Liberation Mono', 28 | 'Liberation Sans', 'Liberation Serif', 'Ubuntu', 'Ubuntu Mono', 29 | 'Noto Sans', 'Noto Serif', 'Droid Sans', 'Droid Serif', 30 | 'FreeMono', 'FreeSans', 'FreeSerif', 'Nimbus Roman', 'Nimbus Sans', 31 | 32 | // Popular Web Fonts 33 | 'Roboto', 'Open Sans', 'Lato', 'Montserrat', 'Source Sans Pro', 34 | 'Raleway', 'PT Sans', 'Noto Sans', 'Ubuntu', 'Nunito', 35 | 'Playfair Display', 'Poppins', 'Merriweather', 'Oswald', 36 | 'Quicksand', 'Dancing Script', 'Pacifico' 37 | ]; 38 | 39 | const testString = 'mmmmmmmmmmlli'; 40 | const testSize = '72px'; 41 | const canvas = document.createElement('canvas'); 42 | const context = canvas.getContext('2d'); 43 | 44 | if (!context) { 45 | return { 46 | fonts: [], 47 | total: 0 48 | }; 49 | } 50 | 51 | const detectFont = (font: string): boolean => { 52 | const baseWidth: Record = {}; 53 | baseFonts.forEach(baseFont => { 54 | context.font = `${testSize} ${baseFont}`; 55 | baseWidth[baseFont] = context.measureText(testString).width; 56 | }); 57 | 58 | let detected = false; 59 | for (const baseFont of baseFonts) { 60 | context.font = `${testSize} "${font}", ${baseFont}`; 61 | const width = context.measureText(testString).width; 62 | if (width !== baseWidth[baseFont]) { 63 | detected = true; 64 | break; 65 | } 66 | } 67 | return detected; 68 | }; 69 | 70 | const detectedFonts = fontList.filter(font => detectFont(font)); 71 | 72 | return { 73 | fonts: detectedFonts, 74 | total: detectedFonts.length 75 | }; 76 | }; 77 | 78 | return { 79 | getFonts 80 | }; 81 | } -------------------------------------------------------------------------------- /app/composables/useSystemInfo.ts: -------------------------------------------------------------------------------- 1 | interface BatteryInfo { 2 | charging: boolean; 3 | chargingTime: number; 4 | dischargingTime: number; 5 | level: number; 6 | } 7 | 8 | interface NavigatorExtended extends Navigator { 9 | deviceMemory?: number; 10 | hardwareConcurrency: number; 11 | userAgentData?: { 12 | platform: string; 13 | brands: Array<{ brand: string; version: string }>; 14 | mobile: boolean; 15 | }; 16 | getBattery?: () => Promise; 17 | } 18 | 19 | interface BatteryManager { 20 | charging: boolean; 21 | chargingTime: number; 22 | dischargingTime: number; 23 | level: number; 24 | } 25 | 26 | export function useSystemInfo() { 27 | const preferredLanguages = usePreferredLanguages() 28 | const memory = useMemory() 29 | const battery = useBattery() 30 | const { pixelRatio } = useDevicePixelRatio() 31 | const screenOrientation = useScreenOrientation() 32 | const windowSize = useWindowSize() 33 | 34 | const getSystemInfo = async () => { 35 | if (!import.meta.client) { 36 | return { 37 | osInfo: 'Unknown', 38 | cpuCores: 0, 39 | deviceMemory: 0, 40 | batteryInfo: null, 41 | bluetoothAvailable: false, 42 | } 43 | } 44 | 45 | const nav = navigator as NavigatorExtended 46 | let osInfo = 'Unknown' 47 | let cpuCores = nav.hardwareConcurrency 48 | 49 | // Attempt to get more accurate OS information 50 | if (nav.userAgentData) { 51 | osInfo = nav.userAgentData.platform 52 | } else { 53 | const userAgent = nav.userAgent.toLowerCase() 54 | if (userAgent.includes('win')) osInfo = 'Windows' 55 | else if (userAgent.includes('mac')) osInfo = 'macOS' 56 | else if (userAgent.includes('linux')) osInfo = 'Linux' 57 | else if (userAgent.includes('android')) osInfo = 'Android' 58 | else if (userAgent.includes('ios')) osInfo = 'iOS' 59 | } 60 | 61 | return { 62 | osInfo, 63 | cpuCores, 64 | deviceMemory: memory.memory.value?.jsHeapSizeLimit || 0, 65 | batteryInfo: battery.isSupported.value ? { 66 | charging: battery.charging.value, 67 | chargingTime: battery.chargingTime.value, 68 | dischargingTime: battery.dischargingTime.value, 69 | level: battery.level.value, 70 | } : null, 71 | bluetoothAvailable: 'bluetooth' in nav, 72 | languages: preferredLanguages.value, 73 | } 74 | } 75 | 76 | const getScreenInfo = () => { 77 | if (!import.meta.client) { 78 | return { 79 | width: 0, 80 | height: 0, 81 | colorDepth: 0, 82 | pixelRatio: 1, 83 | orientation: 'unknown', 84 | windowWidth: 0, 85 | windowHeight: 0, 86 | } 87 | } 88 | 89 | return { 90 | width: window.screen.width, 91 | height: window.screen.height, 92 | colorDepth: window.screen.colorDepth, 93 | pixelRatio: pixelRatio.value, 94 | orientation: screenOrientation.orientation?.value || 'unknown', 95 | windowWidth: windowSize.width.value, 96 | windowHeight: windowSize.height.value, 97 | } 98 | } 99 | 100 | return { 101 | getSystemInfo, 102 | getScreenInfo 103 | } 104 | } -------------------------------------------------------------------------------- /app/components/AudioVisualizer.vue: -------------------------------------------------------------------------------- 1 | 104 | 105 | -------------------------------------------------------------------------------- /app/components/ui/chart-line/LineChart.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 106 | -------------------------------------------------------------------------------- /content/privacy-tools.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Privacy Tools 3 | description: Enhance your online privacy and reduce your browser fingerprint with these recommended tools and settings. 4 | icon: mdi:shield-lock 5 | 6 | warning: 7 | title: Important Note About Spoofing 8 | description: We don't recommend using fingerprint spoofers as they can be easily detected, potentially making you more identifiable. The best approach is to block tracking scripts and use privacy-focused tools. 9 | 10 | categories: 11 | - title: Browser Extensions 12 | tools: 13 | - name: uBlock Origin 14 | icon: mdi:shield-check 15 | description: Efficient blocker for ads, trackers, and other privacy-invasive elements. 16 | link: https://ublockorigin.com/ 17 | chromeLink: https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm 18 | firefoxLink: https://addons.mozilla.org/firefox/addon/ublock-origin/ 19 | recommended: true 20 | 21 | - name: Privacy Badger 22 | icon: mdi:paw 23 | description: Automatically learns to block invisible trackers based on their behavior. 24 | link: https://privacybadger.org/ 25 | chromeLink: https://chrome.google.com/webstore/detail/privacy-badger/pkehgijcmpdhfbdbbnkijodmdjhbjlgp 26 | firefoxLink: https://addons.mozilla.org/firefox/addon/privacy-badger17/ 27 | recommended: true 28 | 29 | - name: Ghostery 30 | icon: mdi:ghost 31 | description: Protect your privacy by blocking trackers and visualizing who is watching you. 32 | link: https://www.ghostery.com/ 33 | chromeLink: https://chrome.google.com/webstore/detail/ghostery/mlomiejdfkolichcflejclcbmpeaniij 34 | firefoxLink: https://addons.mozilla.org/firefox/addon/ghostery/ 35 | recommended: true 36 | 37 | - title: Privacy-Focused Browsers 38 | tools: 39 | - name: Brave Browser 40 | icon: mdi:shield-crown 41 | description: Privacy-focused browser with built-in ad and tracker blocking. 42 | link: https://brave.com/ 43 | 44 | - name: Tor Browser 45 | icon: mdi:web-box 46 | description: Provides anonymity by routing your traffic through the Tor network. 47 | link: https://www.torproject.org/ 48 | 49 | - name: Mullvad Browser 50 | icon: mdi:shield-lock 51 | description: A privacy-focused browser that routes your traffic through the Tor network. 52 | link: https://mullvad.net/browser/ 53 | recommended: true 54 | 55 | - title: Search engine, browser and operating systems 56 | tools: 57 | - name: Tails 58 | icon: mdi:laptop 59 | description: The Amnesic Incognito Live System. Leaves no trace on your computer and routes all traffic through Tor. 60 | link: https://tails.net/ 61 | recommended: true 62 | 63 | - name: Whonix 64 | icon: mdi:shield-lock 65 | description: Operating system focused on anonymity, privacy and security. Routes all connections through the Tor network. 66 | link: https://www.whonix.org/ 67 | 68 | - name: DuckDuckGo 69 | icon: mdi:shield-check 70 | description: Privacy-focused search engine that doesn't track you. 71 | link: https://duckduckgo.com/ 72 | 73 | - name: Qwant 74 | icon: mdi:shield-check 75 | description: Privacy-focused search engine that doesn't track you. 76 | link: https://www.qwant.com/ 77 | recommended: true 78 | 79 | bestPractices: 80 | title: Privacy Best Practices 81 | description: Following these practices can significantly improve your online privacy 82 | columns: 83 | - items: 84 | - text: Regularly clear browser cookies and cache 85 | - text: Use different browsers for different activities 86 | - text: Keep software and extensions updated 87 | - text: Use privacy-focused search engines 88 | - items: 89 | - text: Enable HTTPS-only mode in your browser 90 | - text: Use strong, unique passwords 91 | - text: Enable two-factor authentication 92 | - text: Avoid logging in to services unnecessarily 93 | --- -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | You are an expert in Vue 3, Nuxt 4, TypeScript, Node.js, Vite, Vue Router, VueUse, shadcn-vue, and Tailwind CSS. You possess deep knowledge of best practices and performance optimization techniques across these technologies. 2 | 3 | Code Style and Structure 4 | - Write clean, maintainable, and technically accurate TypeScript code. 5 | - Prioritize functional and declarative programming patterns; avoid using classes. 6 | - Emphasize iteration and modularization to follow DRY principles and minimize code duplication. 7 | - Always use Composition API with Typescript: ` 55 | 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 🔍 Browser Fingerprint Analyzer 4 | 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | [![Nuxt](https://img.shields.io/badge/Nuxt-3.x-00DC82.svg?logo=nuxt.js)](https://nuxt.com/) 7 | [![Vue.js](https://img.shields.io/badge/Vue.js-3.x-4FC08D.svg?logo=vue.js)](https://vuejs.org/) 8 | [![TailwindCSS](https://img.shields.io/badge/TailwindCSS-3.x-38B2AC.svg?logo=tailwind-css)](https://tailwindcss.com/) 9 | 10 | A modern web application built with Nuxt 3 that analyzes and displays your browser's unique fingerprint. This tool demonstrates various browser fingerprinting techniques and provides detailed insights about your browser's characteristics. 11 | 12 | [Demo](https://trackme.dev) · [Report Bug](https://github.com/LeonKohli/browser-fingerprint/issues) · [Request Feature](https://github.com/LeonKohli/browser-fingerprint/issues) · [Support Project ☕](https://www.buymeacoffee.com/LeonKohli) 13 | 14 |
15 | 16 | ## ✨ Features 17 | 18 | ### 🚀 Core Features 19 | - Real-time browser fingerprint generation 20 | - Detailed analysis of browser characteristics 21 | - Entropy score calculation 22 | - Dark mode support 23 | - Responsive design 24 | - Interactive UI components 25 | - Comprehensive system information display 26 | 27 | ### 📊 Analysis Categories 28 | - Browser Information 29 | - System Details 30 | - Hardware Specs 31 | - Media Capabilities 32 | - Network Analysis 33 | - Security Features 34 | 35 | 36 | ## 🛠️ Tech Stack 37 | 38 |
39 | Click to expand 40 | 41 | - **Framework**: [Nuxt.js 3](https://nuxt.com) - Vue-powered SSR framework 42 | - **UI Components**: [shadcn-vue](https://www.shadcn-vue.com/) - Beautifully designed components 43 | - **Styling**: [TailwindCSS](https://tailwindcss.com) - Utility-first CSS framework 44 | - **Icons**: [@nuxt/icon](https://github.com/nuxt-modules/icon) - Icon module for Nuxt 45 | - **State Management**: Vue 3 Composition API 46 | - **Color Mode**: [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) - Dark/Light mode support 47 | - **Utility Functions**: [@vueuse/core](https://vueuse.org/) - Collection of Vue Composition Utilities 48 | 49 |
50 | 51 | ## 🚀 Getting Started 52 | 53 | ### Prerequisites 54 | 55 | Before you begin, ensure you have the following installed: 56 | - Node.js (Latest LTS version recommended) 57 | - Package manager of your choice: 58 | - [bun](https://bun.sh/) (recommended) 59 | 60 | ### 📥 Installation 61 | 62 | 1. Clone the repository 63 | ```bash 64 | git clone https://github.com/LeonKohli/browser-fingerprint.git 65 | cd browser-fingerprint 66 | ``` 67 | 68 | 2. Install dependencies 69 | ```bash 70 | # Using bun 71 | bun install 72 | ``` 73 | 74 | ### 💻 Development 75 | 76 | Start the development server on `http://localhost:3000`: 77 | 78 | ```bash 79 | bun dev 80 | ``` 81 | 82 | ### 🏗️ Production 83 | 84 | Build and preview the production build: 85 | 86 | ```bash 87 | # Build 88 | bun build 89 | 90 | # Preview 91 | bun preview 92 | ``` 93 | 94 | ## 🔍 Features in Detail 95 | 96 |
97 | 🌐 Browser Information 98 | 99 | - Browser name and version detection 100 | - User agent analysis 101 | - Platform identification 102 | - Cookie and privacy settings detection 103 |
104 | 105 |
106 | 💻 System Information 107 | 108 | - Operating system detection 109 | - Timezone information 110 | - Font enumeration 111 | - System capabilities 112 |
113 | 114 |
115 | ⚙️ Hardware Information 116 | 117 | - Screen resolution and properties 118 | - Device memory 119 | - CPU cores 120 | - Hardware concurrency 121 |
122 | 123 |
124 | 🎮 Media Capabilities 125 | 126 | - Canvas fingerprinting 127 | - Audio fingerprinting 128 | - WebGL information 129 | - Media support detection 130 |
131 | 132 | ## 🤝 Contributing 133 | 134 | We welcome contributions! Here's how you can help: 135 | 136 | 1. Fork the repository 137 | 2. Create your feature branch 138 | ```bash 139 | git checkout -b feature/AmazingFeature 140 | ``` 141 | 3. Commit your changes 142 | ```bash 143 | git commit -m '✨ Add some AmazingFeature' 144 | ``` 145 | 4. Push to the branch 146 | ```bash 147 | git push origin feature/AmazingFeature 148 | ``` 149 | 5. Open a Pull Request 150 | 151 | ## 📄 License 152 | 153 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 154 | 155 | ## 🔒 Privacy Notice 156 | 157 | > **Important**: This application demonstrates browser fingerprinting techniques for educational purposes only. 158 | 159 | The fingerprint generated can potentially be used to track users across websites, even in private browsing mode or when using VPN services. No data is stored or transmitted to external servers. 160 | 161 | 162 | --- 163 | 164 |
165 | 166 | Made with ❤️ by [LeonKohli](https://github.com/LeonKohli) 167 | 168 | If you find this project helpful, consider [buying me a coffee ☕](https://www.buymeacoffee.com/LeonKohli) 169 | 170 |
171 | -------------------------------------------------------------------------------- /app/components/FingerprintInfoModal.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 109 | 110 | -------------------------------------------------------------------------------- /app/composables/useBrowserFingerprint.ts: -------------------------------------------------------------------------------- 1 | export function useBrowserFingerprint() { 2 | const { language } = useNavigatorLanguage() 3 | const preferredLanguages = usePreferredLanguages() 4 | const memory = useMemory() 5 | const battery = useBattery() 6 | 7 | const { getAudioFingerprint } = useAudioFingerprint() 8 | const { generateCanvasFingerprint } = useCanvasFingerprint() 9 | const { getWebGLFingerprint } = useWebGLFingerprint() 10 | const { getSystemInfo, getScreenInfo } = useSystemInfo() 11 | const { getHardwareAcceleration, getTouchSupport, getColorDepth } = useHardwareInfo() 12 | const { getFonts } = useFontDetection() 13 | const { detectIncognito } = useIncognitoDetection() 14 | const { calculateEntropy } = useEntropyCalculator() 15 | const { formatKey, copyFingerprint, getSectionDescription } = useFingerprintHelpers() 16 | 17 | const detectBrowserInfo = () => { 18 | const ua = navigator.userAgent 19 | let browserName = 'Unknown' 20 | let version = 'Unknown' 21 | 22 | if (ua.match(/chrome|chromium|crios/i)) { 23 | browserName = ua.match(/edg/i) ? 'Edge' : 'Chrome' 24 | } else if (ua.match(/firefox|fxios/i)) { 25 | browserName = 'Firefox' 26 | } else if (ua.match(/safari/i)) { 27 | browserName = 'Safari' 28 | } else if (ua.match(/opr\//i)) { 29 | browserName = 'Opera' 30 | } else if (ua.match(/trident/i)) { 31 | browserName = 'Internet Explorer' 32 | } 33 | 34 | const match = ua.match(/(version|edge|chrome|firefox|safari|opr)\/?\s*(\.?\d+(\.\d+)*)/i) 35 | if (match && match[2]) { 36 | version = match[2] 37 | } 38 | 39 | return { browserName, version } 40 | } 41 | 42 | const detectPrivateMode = async (): Promise => { 43 | const { isPrivate } = await detectIncognito() 44 | return isPrivate 45 | } 46 | 47 | const generateFingerprint = async () => { 48 | const fp: any = {} 49 | const sectionIssues: Record = { 50 | browser: false, 51 | system: false, 52 | screen: false, 53 | media: false, 54 | webgl: false, 55 | hardware: false 56 | } 57 | 58 | try { 59 | // Collect browser information 60 | const browserInfo = detectBrowserInfo() 61 | fp.browser = { 62 | userAgent: navigator.userAgent, 63 | language: language.value, 64 | languages: preferredLanguages.value, 65 | platform: navigator.platform, 66 | cookiesEnabled: navigator.cookieEnabled, 67 | doNotTrack: navigator.doNotTrack || null, 68 | vendor: navigator.vendor || 'Unknown', 69 | browserName: browserInfo.browserName, 70 | browserVersion: browserInfo.version, 71 | isPrivateMode: await detectPrivateMode(), 72 | plugins: Array.from(navigator.plugins) 73 | .map(p => ({ 74 | name: p.name, 75 | description: p.description, 76 | })) 77 | .filter(p => p.name || p.description), 78 | } 79 | 80 | if (memory.memory.value) { 81 | fp.system = { 82 | ...fp.system, 83 | memory: memory.memory.value 84 | } 85 | } 86 | 87 | if (battery.isSupported.value) { 88 | fp.system = { 89 | ...fp.system, 90 | battery: { 91 | charging: battery.charging.value, 92 | chargingTime: battery.chargingTime.value, 93 | dischargingTime: battery.dischargingTime.value, 94 | level: battery.level.value 95 | } 96 | } 97 | } 98 | } catch (error) { 99 | console.error('Error collecting browser info:', error) 100 | sectionIssues.browser = true 101 | } 102 | 103 | try { 104 | // Collect system and screen information 105 | const systemInfo = await getSystemInfo() 106 | const screenInfo = getScreenInfo() 107 | fp.system = { 108 | ...systemInfo, 109 | } 110 | fp.screen = { 111 | ...screenInfo, 112 | } 113 | } catch (error) { 114 | console.error('Error collecting system/screen info:', error) 115 | sectionIssues.system = true 116 | sectionIssues.screen = true 117 | } 118 | 119 | try { 120 | // Collect media information 121 | fp.media = { 122 | canvasFingerprint: await generateCanvasFingerprint(), 123 | audioFingerprint: await getAudioFingerprint(), 124 | } 125 | } catch (error) { 126 | console.error('Error collecting media info:', error) 127 | sectionIssues.media = true 128 | } 129 | 130 | try { 131 | // Collect WebGL information 132 | fp.webgl = getWebGLFingerprint() 133 | } catch (error) { 134 | console.error('Error collecting WebGL info:', error) 135 | sectionIssues.webgl = true 136 | } 137 | 138 | try { 139 | // Collect hardware information 140 | fp.hardware = { 141 | hardwareAcceleration: getHardwareAcceleration(), 142 | touchSupport: getTouchSupport(), 143 | colorDepth: getColorDepth(), 144 | } 145 | } catch (error) { 146 | console.error('Error collecting hardware info:', error) 147 | sectionIssues.hardware = true 148 | } 149 | 150 | try { 151 | // Collect font information 152 | fp.fonts = getFonts() 153 | } catch (error) { 154 | console.error('Error collecting font info:', error) 155 | sectionIssues.fonts = true 156 | } 157 | 158 | // Calculate entropy 159 | const entropyScore = calculateEntropy(fp) 160 | 161 | // Generate hash 162 | const fingerprintString = JSON.stringify(fp) 163 | const encoder = new TextEncoder() 164 | const data = encoder.encode(fingerprintString) 165 | const hashBuffer = await crypto.subtle.digest('SHA-256', data) 166 | const hashArray = Array.from(new Uint8Array(hashBuffer)) 167 | fp.hash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('') 168 | 169 | return { fingerprint: fp, entropyScore, sectionIssues } 170 | } 171 | 172 | return { 173 | generateFingerprint, 174 | formatKey, 175 | copyFingerprint, 176 | getSectionDescription, 177 | detectBrowserInfo, 178 | detectPrivateMode, 179 | calculateEntropy, 180 | } 181 | } -------------------------------------------------------------------------------- /app/components/StatsBar.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | -------------------------------------------------------------------------------- /app/pages/links.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | -------------------------------------------------------------------------------- /app/composables/useIncognitoDetection.ts: -------------------------------------------------------------------------------- 1 | // This code is adapted from https://github.com/Joe12387/detectIncognito. 2 | 3 | interface IncognitoDetectionResult { 4 | isPrivate: boolean; 5 | browserName: string; 6 | } 7 | 8 | export function useIncognitoDetection() { 9 | const detectIncognito = async (): Promise => { 10 | if (!import.meta.client) { 11 | return { isPrivate: false, browserName: 'Unknown' }; 12 | } 13 | 14 | return new Promise((resolve, reject) => { 15 | let browserName = 'Unknown'; 16 | 17 | const __callback = (isPrivate: boolean): void => { 18 | resolve({ 19 | isPrivate, 20 | browserName 21 | }); 22 | }; 23 | 24 | const identifyChromium = (): string => { 25 | const ua = navigator.userAgent; 26 | if (ua.match(/Chrome/)) { 27 | if ((navigator as any).brave !== undefined) { 28 | return 'Brave'; 29 | } else if (ua.match(/Edg/)) { 30 | return 'Edge'; 31 | } else if (ua.match(/OPR/)) { 32 | return 'Opera'; 33 | } 34 | return 'Chrome'; 35 | } else { 36 | return 'Chromium'; 37 | } 38 | }; 39 | 40 | const assertEvalToString = (value: number): boolean => { 41 | return value === eval.toString().length; 42 | }; 43 | 44 | const feid = (): number => { 45 | let toFixedEngineID = 0; 46 | try { 47 | eval(`(-1).toFixed(-1);`); 48 | } catch (e) { 49 | toFixedEngineID = (e as Error).message.length; // Safari 44, Chrome 51, Firefox 25 50 | } 51 | return toFixedEngineID; 52 | }; 53 | 54 | const isSafari = (): boolean => feid() === 44; 55 | const isChrome = (): boolean => feid() === 51; 56 | const isFirefox = (): boolean => feid() === 25; 57 | const isMSIE = (): boolean => (navigator as any).msSaveBlob !== undefined && assertEvalToString(39); 58 | 59 | const newSafariTest = (): void => { 60 | const tmp_name = String(Math.random()); 61 | 62 | try { 63 | const db = window.indexedDB.open(tmp_name, 1); 64 | 65 | db.onupgradeneeded = function (i) { 66 | const res = i.target as IDBDatabase; 67 | 68 | try { 69 | res.createObjectStore('test', { 70 | autoIncrement: true 71 | }).put(new Blob()); 72 | 73 | __callback(false); 74 | } catch (e) { 75 | let message = e; 76 | 77 | if (e instanceof Error) { 78 | message = e.message ?? e; 79 | } 80 | 81 | if (typeof message !== 'string') { 82 | __callback(false); return; 83 | } 84 | 85 | const matchesExpectedError = message.includes('BlobURLs are not yet supported'); 86 | 87 | __callback(matchesExpectedError); return; 88 | } finally { 89 | res.close(); 90 | window.indexedDB.deleteDatabase(tmp_name); 91 | } 92 | }; 93 | } catch (e) { 94 | __callback(false); 95 | } 96 | }; 97 | 98 | const oldSafariTest = (): void => { 99 | const openDB = (window as any).openDatabase; 100 | const storage = window.localStorage; 101 | try { 102 | openDB(null, null, null, null); 103 | } catch (e) { 104 | __callback(true); return; 105 | } 106 | try { 107 | storage.setItem('test', '1'); 108 | storage.removeItem('test'); 109 | } catch (e) { 110 | __callback(true); return; 111 | } 112 | __callback(false); 113 | }; 114 | 115 | const safariPrivateTest = (): void => { 116 | if (navigator.maxTouchPoints !== undefined) { 117 | newSafariTest(); 118 | } else { 119 | oldSafariTest(); 120 | } 121 | }; 122 | 123 | const getQuotaLimit = (): number => { 124 | const w = window as any; 125 | if ( 126 | w.performance !== undefined && 127 | w.performance.memory !== undefined && 128 | w.performance.memory.jsHeapSizeLimit !== undefined 129 | ) { 130 | return (performance as any).memory.jsHeapSizeLimit; 131 | } 132 | return 1073741824; 133 | }; 134 | 135 | const storageQuotaChromePrivateTest = (): void => { 136 | (navigator as any).webkitTemporaryStorage.queryUsageAndQuota( 137 | function (_: number, quota: number) { 138 | const quotaInMib = Math.round(quota / (1024 * 1024)); 139 | const quotaLimitInMib = Math.round(getQuotaLimit() / (1024 * 1024)) * 2; 140 | 141 | __callback(quotaInMib < quotaLimitInMib); 142 | }, 143 | function (e: any) { 144 | reject( 145 | new Error( 146 | 'detectIncognito somehow failed to query storage quota: ' + 147 | e.message 148 | ) 149 | ); 150 | } 151 | ); 152 | }; 153 | 154 | const oldChromePrivateTest = (): void => { 155 | const fs = (window as any).webkitRequestFileSystem; 156 | const success = function () { 157 | __callback(false); 158 | }; 159 | const error = function () { 160 | __callback(true); 161 | }; 162 | fs(0, 1, success, error); 163 | }; 164 | 165 | const chromePrivateTest = (): void => { 166 | if (self.Promise !== undefined && (self.Promise as any).allSettled !== undefined) { 167 | storageQuotaChromePrivateTest(); 168 | } else { 169 | oldChromePrivateTest(); 170 | } 171 | }; 172 | 173 | const firefoxPrivateTest = (): void => { 174 | __callback(navigator.serviceWorker === undefined); 175 | }; 176 | 177 | const msiePrivateTest = (): void => { 178 | __callback(window.indexedDB === undefined); 179 | }; 180 | 181 | const main = (): void => { 182 | if (isSafari()) { 183 | browserName = 'Safari'; 184 | safariPrivateTest(); 185 | } else if (isChrome()) { 186 | browserName = identifyChromium(); 187 | chromePrivateTest(); 188 | } else if (isFirefox()) { 189 | browserName = 'Firefox'; 190 | firefoxPrivateTest(); 191 | } else if (isMSIE()) { 192 | browserName = 'Internet Explorer'; 193 | msiePrivateTest(); 194 | } else { 195 | reject(new Error('detectIncognito cannot determine the browser')); 196 | } 197 | }; 198 | 199 | main(); 200 | }); 201 | }; 202 | 203 | return { 204 | detectIncognito 205 | }; 206 | } 207 | 208 | if (import.meta.client) { 209 | (window as any).detectIncognito = useIncognitoDetection().detectIncognito; 210 | } -------------------------------------------------------------------------------- /app/composables/useEntropyCalculator.ts: -------------------------------------------------------------------------------- 1 | export function useEntropyCalculator() { 2 | const calculateEntropy = (fp: any): number => { 3 | if (!fp) return 0; 4 | 5 | let totalEntropy = 0; 6 | 7 | // Browser features 8 | if (fp.browser) { 9 | // User Agent 10 | if (fp.browser.userAgent) { 11 | totalEntropy += estimateUserAgentEntropy(fp.browser.userAgent); 12 | } 13 | // Language 14 | if (fp.browser.language) { 15 | totalEntropy += estimateLanguageEntropy(fp.browser.language); 16 | } 17 | // Platform 18 | if (fp.browser.platform) { 19 | totalEntropy += estimatePlatformEntropy(fp.browser.platform); 20 | } 21 | // Plugins 22 | if (fp.browser.plugins && fp.browser.plugins.length >= 0) { 23 | totalEntropy += estimatePluginsEntropy(fp.browser.plugins); 24 | } 25 | // Do Not Track 26 | if (fp.browser.doNotTrack !== null) { 27 | totalEntropy += estimateDoNotTrackEntropy(fp.browser.doNotTrack); 28 | } 29 | } 30 | 31 | // System features 32 | if (fp.system) { 33 | // Timezone 34 | if (fp.system.timezone) { 35 | totalEntropy += estimateTimezoneEntropy(fp.system.timezone); 36 | } 37 | // Fonts 38 | if (fp.fonts && fp.fonts.fonts && fp.fonts.fonts.length > 0) { 39 | totalEntropy += estimateFontsEntropy(fp.fonts.fonts); 40 | } 41 | } 42 | 43 | // Screen features 44 | if (fp.screen) { 45 | // Screen resolution 46 | if (fp.screen.width && fp.screen.height) { 47 | totalEntropy += estimateScreenResolutionEntropy(fp.screen.width, fp.screen.height); 48 | } 49 | // Color depth 50 | if (fp.screen.colorDepth) { 51 | totalEntropy += estimateColorDepthEntropy(fp.screen.colorDepth); 52 | } 53 | } 54 | 55 | // Media features 56 | if (fp.media) { 57 | // Canvas fingerprint 58 | if (fp.media.canvasFingerprint) { 59 | totalEntropy += estimateCanvasFingerprintEntropy(fp.media.canvasFingerprint); 60 | } 61 | // Audio fingerprint 62 | if (fp.media.audioFingerprint) { 63 | totalEntropy += estimateAudioFingerprintEntropy(fp.media.audioFingerprint); 64 | } 65 | } 66 | 67 | // WebGL features 68 | if (fp.webgl) { 69 | // Renderer 70 | if (fp.webgl.renderer) { 71 | totalEntropy += estimateWebGLRendererEntropy(fp.webgl.renderer); 72 | } 73 | } 74 | 75 | // Hardware features 76 | if (fp.hardware) { 77 | // Device Memory 78 | if (fp.hardware.deviceMemory) { 79 | totalEntropy += estimateDeviceMemoryEntropy(fp.hardware.deviceMemory); 80 | } 81 | // CPU Cores 82 | if (fp.hardware.cpuCores) { 83 | totalEntropy += estimateCpuCoresEntropy(fp.hardware.cpuCores); 84 | } 85 | // Touch support 86 | if (fp.hardware.touchSupport) { 87 | totalEntropy += estimateTouchSupportEntropy(fp.hardware.touchSupport); 88 | } 89 | // Battery 90 | if (fp.system && fp.system.batteryInfo) { 91 | totalEntropy += estimateBatteryEntropy(fp.system.batteryInfo); 92 | } 93 | } 94 | 95 | console.log('Total Entropy:', totalEntropy); 96 | return totalEntropy; 97 | }; 98 | 99 | // Entropy estimation functions with dynamic calculations 100 | 101 | const estimateUserAgentEntropy = (userAgent: string): number => { 102 | const commonUserAgents = [ 103 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 104 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', 105 | 'Mozilla/5.0 (X11; Linux x86_64)', 106 | ]; 107 | if (commonUserAgents.includes(userAgent)) { 108 | return 1; // Low entropy 109 | } else { 110 | return 4; // Higher entropy 111 | } 112 | }; 113 | 114 | const estimateLanguageEntropy = (language: string): number => { 115 | const commonLanguages = ['en-US', 'en', 'zh-CN', 'es', 'fr', 'de', 'ja', 'ru', 'pt', 'it']; 116 | if (commonLanguages.includes(language)) { 117 | return 1; // Low entropy 118 | } else { 119 | return 3; // Higher entropy 120 | } 121 | }; 122 | 123 | const estimatePlatformEntropy = (platform: string): number => { 124 | const commonPlatforms = ['Win32', 'MacIntel', 'Linux x86_64']; 125 | if (commonPlatforms.includes(platform)) { 126 | return 1; // Low entropy 127 | } else { 128 | return 2; // Higher entropy 129 | } 130 | }; 131 | 132 | const estimatePluginsEntropy = (plugins: any[]): number => { 133 | const numPlugins = plugins.length; 134 | if (numPlugins === 0) { 135 | return 1; // No plugins 136 | } else if (numPlugins <= 3) { 137 | return 2; // Few plugins 138 | } else if (numPlugins <= 6) { 139 | return 3; // Moderate number 140 | } else { 141 | return 4; // Many plugins 142 | } 143 | }; 144 | 145 | const estimateDoNotTrackEntropy = (doNotTrack: string | null): number => { 146 | return 1; // Minimal entropy 147 | }; 148 | 149 | const estimateTimezoneEntropy = (timezone: string): number => { 150 | const commonTimezones = [ 151 | 'UTC', 152 | 'GMT', 153 | 'America/New_York', 154 | 'Europe/London', 155 | 'Asia/Shanghai', 156 | ]; 157 | if (commonTimezones.includes(timezone)) { 158 | return 1; // Low entropy 159 | } else { 160 | return 2; // Higher entropy 161 | } 162 | }; 163 | 164 | const estimateFontsEntropy = (fonts: string[]): number => { 165 | const numFonts = fonts.length; 166 | if (numFonts <= 10) { 167 | return 2; 168 | } else if (numFonts <= 20) { 169 | return 3; 170 | } else { 171 | return 4; 172 | } 173 | }; 174 | 175 | const estimateScreenResolutionEntropy = (width: number, height: number): number => { 176 | const commonResolutions = [ 177 | { width: 1920, height: 1080 }, 178 | { width: 1366, height: 768 }, 179 | { width: 1440, height: 900 }, 180 | { width: 1536, height: 864 }, 181 | { width: 1280, height: 720 }, 182 | ]; 183 | const isCommon = commonResolutions.some( 184 | (res) => res.width === width && res.height === height 185 | ); 186 | return isCommon ? 1 : 3; 187 | }; 188 | 189 | const estimateColorDepthEntropy = (colorDepth: number): number => { 190 | if (colorDepth === 24 || colorDepth === 30) { 191 | return 1; 192 | } else { 193 | return 2; 194 | } 195 | }; 196 | 197 | const estimateCanvasFingerprintEntropy = (canvasFingerprint: string): number => { 198 | return 10; // High entropy 199 | }; 200 | 201 | const estimateAudioFingerprintEntropy = (audioFingerprint: any): number => { 202 | return 10; // High entropy 203 | }; 204 | 205 | const estimateWebGLRendererEntropy = (renderer: string): number => { 206 | const commonRenderers = ['ANGLE', 'Intel', 'AMD', 'NVIDIA']; 207 | if (commonRenderers.some((common) => renderer.includes(common))) { 208 | return 2; 209 | } else { 210 | return 4; 211 | } 212 | }; 213 | 214 | const estimateDeviceMemoryEntropy = (deviceMemory: number): number => { 215 | const commonMemoryValues = [4, 8, 16]; 216 | if (commonMemoryValues.includes(deviceMemory)) { 217 | return 1; 218 | } else { 219 | return 2; 220 | } 221 | }; 222 | 223 | const estimateCpuCoresEntropy = (cpuCores: number): number => { 224 | const commonCoreCounts = [2, 4, 8]; 225 | if (commonCoreCounts.includes(cpuCores)) { 226 | return 1; 227 | } else { 228 | return 2; 229 | } 230 | }; 231 | 232 | const estimateTouchSupportEntropy = (touchSupport: any): number => { 233 | return 1; 234 | }; 235 | 236 | const estimateBatteryEntropy = (batteryInfo: any): number => { 237 | const batteryLevel = batteryInfo.level; 238 | if (batteryLevel >= 0.9) { 239 | return 1; 240 | } else { 241 | return 2; 242 | } 243 | }; 244 | 245 | return { 246 | calculateEntropy, 247 | }; 248 | } -------------------------------------------------------------------------------- /content/links.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Useful Links 3 | description: Explore comprehensive resources about browser fingerprinting, from technical papers to press articles. 4 | icon: mdi:link-variant 5 | 6 | sections: 7 | - title: General Definitions 8 | type: links 9 | links: 10 | - title: Mozilla's Browser Fingerprinting Guide 11 | description: Developer documentation on fingerprinting protection 12 | url: https://developer.mozilla.org/en-US/docs/Glossary/Fingerprinting 13 | icon: mdi:firefox 14 | 15 | - title: Device Fingerprint (Wikipedia) 16 | description: Wikipedia article explaining device fingerprinting concepts 17 | url: https://en.wikipedia.org/wiki/Device_fingerprint 18 | icon: mdi:wikipedia 19 | 20 | - title: Press Articles & Presentations 21 | type: links 22 | links: 23 | - title: Browser 'fingerprints' help track users 24 | description: BBC News coverage on browser fingerprinting technology 25 | url: https://www.bbc.com/news/technology-28423257 26 | icon: mdi:newspaper 27 | 28 | - title: Publicité - Une nouvelle technique pour pister les internautes 29 | description: Le Monde's analysis of new tracking techniques 30 | url: https://www.lemonde.fr/pixels/article/2014/07/22/publicite-une-nouvelle-technique-pour-pister-les-internautes_4461305_4408996.html 31 | icon: mdi:newspaper 32 | 33 | - title: Si on brouillait les pistes avec AmIUnique? 34 | description: Framasoft blog post about fingerprint obfuscation 35 | url: https://framablog.org/2014/12/23/si-on-brouillait-les-pistes-avec-amiunique/ 36 | icon: mdi:newspaper 37 | 38 | - title: Browser Fingerprinting at Open World Forum 2014 39 | description: Video presentation about browser fingerprinting 40 | url: https://www.youtube.com/watch?v=uVECjeSUzy0 41 | icon: mdi:youtube 42 | 43 | - title: Browser Fingerprinting and the Online-Tracking Arms Race 44 | description: IEEE Spectrum's deep dive into browser fingerprinting 45 | url: https://spectrum.ieee.org/browser-fingerprinting-and-the-onlinetracking-arms-race 46 | icon: mdi:newspaper 47 | 48 | - title: Scientific Papers 49 | type: papers 50 | links: 51 | - title: Hot or Not - Revealing Hidden Services by Their Clock Skew 52 | authors: J. Murdoch, Steven 53 | conference: Conference on Computer & Communications Security 54 | year: 2006 55 | url: https://murdoch.is/papers/ccs06hotornot.pdf 56 | 57 | - title: How unique is your web browser? 58 | authors: Peter Eckersley 59 | conference: Conference on Privacy enhancing technologies 60 | year: 2010 61 | url: https://panopticlick.eff.org/static/browser-uniqueness.pdf 62 | 63 | - title: User Tracking on the Web via Cross-Browser Fingerprinting 64 | authors: Boda, Károly & Máté Foldes, Ádám & Gulyás, Gábor & Imre, Sandor 65 | conference: Nordic Conference on Secure IT Systems 66 | year: 2011 67 | url: https://pet-portal.eu/files/articles/2011/fingerprinting/cross-browser_fingerprinting.pdf 68 | 69 | - title: Pixel Perfect - Fingerprinting Canvas in HTML5 70 | authors: Keaton Mowery and Hovav Shacham 71 | conference: Web 2.0 Security & Privacy 72 | year: 2012 73 | url: https://cseweb.ucsd.edu/~hovav/dist/canvas.pdf 74 | 75 | - title: Fast and Reliable Browser Identification with JavaScript Engine Fingerprinting 76 | authors: M. Mulazzani, S. Schrittwieser, P. Reschl, M. Leithner, E. Weippl, M. Huber 77 | conference: Web 2.0 Security & Privacy 78 | year: 2013 79 | url: https://publications.sba-research.org/publications/jsfingerprinting.pdf 80 | 81 | - title: Cookieless Monster - Exploring the Ecosystem of Web-based Device Fingerprinting 82 | authors: N. Nikiforakis, A. Kapravelos, W. Joosen, C. Kruegel, F. Piessens and G. Vigna 83 | conference: Symposium on Security and Privacy 84 | year: 2013 85 | url: https://cs.ucsb.edu/~vigna/publications/2013_SP_cookieless.pdf 86 | 87 | - title: The Web Never Forgets 88 | authors: Acar, Gunes and Eubank, Christian and Englehardt, Steven and Juarez, Marc and Narayanan, Arvind and Diaz, Claudia 89 | conference: Conference on Computer and Communications Security 90 | year: 2014 91 | url: https://www.esat.kuleuven.be/cosic/publications/article-2457.pdf 92 | 93 | - title: The Leaking Battery - A Privacy Analysis of the HTML5 Battery Status API 94 | authors: Olejnik, Lukasz and Acar, Gunes and Castelluccia, Claude and Diaz, Claudia 95 | conference: Workshop on Data Privacy Management, and Security Assurance 96 | year: 2015 97 | url: https://eprint.iacr.org/2015/616.pdf 98 | 99 | - title: Fingerprinting Web Users Through Font Metrics 100 | authors: Fifield, David & Egelman, Serge 101 | conference: Conference on Financial Cryptography and Data Security 102 | year: 2015 103 | url: http://fc15.ifca.ai/preproceedings/paper_83.pdf 104 | 105 | - title: Mobile Device Fingerprinting Considered Harmful for Risk-based Authentication 106 | authors: Spooren, Jan & Preuveneers, Davy & Joosen, Wouter 107 | conference: European Workshop on System Security 108 | year: 2015 109 | url: https://lirias.kuleuven.be/bitstream/123456789/496316/1/a6-spooren.pdf 110 | 111 | - title: Beauty and the Beast - Diverting modern web browsers to build unique browser fingerprints 112 | authors: P. Laperdrix, W. Rudametkin and B. Baudry 113 | conference: Symposium on Security and Privacy 114 | year: 2016 115 | url: https://hal.inria.fr/hal-01285470/file/beauty-sp16.pdf 116 | 117 | - title: (Cross-)Browser Fingerprinting via OS and Hardware Level Features 118 | authors: Cao, Yinzhi & Li, Song & Wijmans, Erik 119 | conference: Network and Distributed System Security Symposium 120 | year: 2017 121 | url: https://www.ndss-symposium.org/wp-content/uploads/2017/09/ndss2017_02B-3_Cao_paper.pdf 122 | 123 | - title: Discovering Browser Extensions via Web Accessible Resources 124 | authors: Sjosten, Alexander and Van Acker, Steven and Sabelfeld, Andrei 125 | conference: Conference on Data and Application Security and Privacy 126 | year: 2017 127 | url: http://www.cse.chalmers.se/research/group/security/publications/2017/extensions/codaspy-17.pdf 128 | 129 | - title: XHOUND - Quantifying the Fingerprintability of Browser Extensions 130 | authors: O. Starov and N. Nikiforakis 131 | conference: Symposium on Security and Privacy 132 | year: 2017 133 | url: https://www.securitee.org/files/xhound-oakland17.pdf 134 | 135 | - title: FP-STALKER - Tracking Browser Fingerprint Evolutions 136 | authors: Antoine Vastel, Pierre Laperdrix, Walter Rudametkin, Romain Rouvoy 137 | conference: Symposium on Security and Privacy 138 | year: 2018 139 | url: https://hal.inria.fr/hal-01652021/document 140 | 141 | - title: FP-Scanner - The Privacy Implications of Browser Fingerprint Inconsistencies 142 | authors: Antoine Vastel, Pierre Laperdrix, Walter Rudametkin, Romain Rouvoy 143 | conference: USENIX Security Symposium 144 | year: 2018 145 | url: https://hal.inria.fr/hal-01820197/document 146 | 147 | - title: Hiding in the Crowd - an Analysis of the Effectiveness of Browser Fingerprinting at Large Scale 148 | authors: Alejandro Gómez-Boix, Pierre Laperdrix, Benoit Baudry 149 | conference: World Wide Web Conference 150 | year: 2018 151 | url: https://hal.inria.fr/hal-01718234v2/document 152 | 153 | - title: Don't count me out - On the relevance of IP addresses in the tracking ecosystem 154 | authors: Vikas Mishra, Pierre Laperdrix, Antoine Vastel, Walter Rudametkin, Romain Rouvoy, Martin Lopatka 155 | conference: The Web Conference 156 | year: 2020 157 | url: https://hal.inria.fr/hal-02435622 158 | 159 | - title: A Survey of Browser Fingerprint Research and Application 160 | authors: Desheng Zhang, Jianhui Zhang, Youjun Bu, Bo Chen, Chongxin Sun, Tianyu Wang 161 | conference: Online Library 162 | year: 2022 163 | url: https://onlinelibrary.wiley.com/doi/epdf/10.1155/2022/3363335 164 | 165 | - title: Tools & Resources 166 | type: links 167 | links: 168 | - title: AmIUnique.org 169 | description: Interactive tool to test your browser fingerprint and learn about its uniqueness 170 | url: https://amiunique.org 171 | icon: mdi:fingerprint 172 | 173 | - title: EFF's Cover Your Tracks 174 | description: Test your browser's defenses against tracking and fingerprinting 175 | url: https://coveryourtracks.eff.org 176 | icon: mdi:shield 177 | 178 | - title: Device Info 179 | description: Detailed technical breakdown of your browser's fingerprint components 180 | url: https://deviceinfo.me 181 | icon: mdi:information 182 | 183 | - title: Privacy.net Browser Check 184 | description: Comprehensive browser fingerprinting test with detailed explanations 185 | url: https://privacy.net/analyzer 186 | icon: mdi:security 187 | 188 | - title: FingerprintJS 189 | description: The most popular open-source fingerprinting library 190 | url: https://fingerprint.com/ 191 | icon: mdi:code-braces 192 | 193 | - title: Brave Browser 194 | description: Brave's documentation on fingerprinting protection mechanisms 195 | url: https://brave.com/privacy-updates/ 196 | icon: mdi:image-filter-center-focus-strong-outline 197 | --- -------------------------------------------------------------------------------- /app/pages/privacy-tools.vue: -------------------------------------------------------------------------------- 1 | 153 | 154 | -------------------------------------------------------------------------------- /design.md: -------------------------------------------------------------------------------- 1 | # Universal UI/UX Design Principles and User Flow 2 | 3 | ## Information Architecture & Cognitive Load 4 | 5 | - **Mental Models** 6 | - Match user expectations from similar applications. 7 | - Limit navigation menus to 5-7 items. 8 | - Group related actions within 8px-16px. 9 | - Limit choices to prevent decision paralysis. 10 | - Use progressive disclosure for complex flows. 11 | 12 | - **Cognitive Patterns** 13 | - Prioritize recognition over recall. 14 | - Provide clear affordances for interactive elements. 15 | - Maintain consistent patterns across similar actions. 16 | - Place common elements in predictable locations. 17 | - Offer visual hints for hidden functionality. 18 | 19 | - **User Flow Efficiency** 20 | - Ensure primary actions are accessible within 1-2 clicks. 21 | - Place critical information above the fold. 22 | - Provide clear escape routes from every screen. 23 | - Include an undo option for destructive actions. 24 | - Implement autosave for forms every 30 seconds. 25 | 26 | ## User Flow & Experience 27 | 28 | - **Quick Actions Flow** 29 | - Group primary actions in easily scannable grids. 30 | - Display a maximum of 4-5 main actions at once. 31 | - Place the most used actions within thumb reach on mobile. 32 | - Highlight critical information (like balances) at the top. 33 | - Hide secondary actions in expandable menus. 34 | 35 | - **Navigation Pattern** 36 | - Eliminate unnecessary navigation elements. 37 | - Keep critical paths within 2-3 clicks. 38 | - Ensure main actions are visible without scrolling. 39 | - Make the back button always accessible. 40 | - Provide clear visual breadcrumbs. 41 | 42 | - **Content Priority** 43 | - Display key information (totals, balances) in the largest size. 44 | - Group action items by frequency of use. 45 | - Place critical notifications at the top of the viewport. 46 | - Collapse secondary information with expandable options. 47 | - Make error states immediately visible. 48 | 49 | - **Interactive Feedback** 50 | - Provide immediate response to user actions (<100ms). 51 | - Show loading states for actions longer than 300ms. 52 | - Display success/error messages within the viewport. 53 | - Use toast notifications lasting 3-5 seconds. 54 | - Include progress indicators for multi-step processes. 55 | 56 | ## Core Design Principles 57 | 58 | ### 1. Good Design is Minimal Design 59 | 60 | - **Focus on Essential Features** 61 | - Remove decorative elements that don't serve a purpose. 62 | - Assign a single clear function to each component. 63 | - Limit to a maximum of 3 primary actions per view. 64 | - Use only 2-3 font families throughout the design. 65 | - Keep navigation to a maximum of 7 main items. 66 | 67 | - **Reduced Color Palette** 68 | - **Primary Color**: Use for 60% of the interface. 69 | - **Secondary Color**: Use for 30% of the interface. 70 | - **Accent Color**: Use for 10% of the interface. 71 | - Limit to 3 primary colors. 72 | - Use 2-3 shades of gray for text hierarchy. 73 | 74 | ### 2. Laws of Similarity and Proximity 75 | 76 | - **Component Consistency** 77 | - **Border Radius**: 4px-8px for small elements, 8px-12px for cards. 78 | - **Button Grouping**: 8px-12px between related actions. 79 | - **Form Fields**: 16px-24px vertical spacing. 80 | - **Card Patterns**: Consistent 16px-24px internal padding. 81 | - **Related Items**: Maximum 8px spacing between them. 82 | 83 | - **Visual Grouping** 84 | - **Section Spacing**: 32px-48px. 85 | - **Related Content**: Maximum 16px separation. 86 | - **Element Grouping**: Group similar elements within 8px proximity. 87 | - **Unrelated Elements**: Minimum 24px separation. 88 | - **Content Blocks**: 32px bottom margin. 89 | 90 | ### 3. Generous Spacing 91 | 92 | - **Padding Scale** 93 | - **Small Components**: 8px-12px. 94 | - **Medium Components**: 16px-24px. 95 | - **Large Components**: 24px-32px. 96 | - **Containers**: 24px-48px. 97 | - **Page Margins**: 32px-64px. 98 | 99 | - **White Space** 100 | - **Paragraph Spacing**: 16px-24px. 101 | - **Section Margins**: 32px-48px. 102 | - **Content Width**: Maximum of 680px-800px. 103 | - **Line Height**: 1.5-1.7 for body text. 104 | - **List Item Spacing**: 8px-12px. 105 | 106 | ### 4. Systematic Design 107 | 108 | - **Base Measurements** 109 | - **Base Unit**: 4px or 8px. 110 | - **Spacing Increments**: Multiples of the base unit. 111 | - **Maximum Container Width**: 1200px-1440px. 112 | - **Minimum Touch Target**: 44px × 44px. 113 | - **Icon Sizes**: 16px, 24px, 32px. 114 | 115 | - **Typography Scale** 116 | - **Base Size**: 16px. 117 | - **Scale Ratio**: 1.2 or 1.25. 118 | - **Minimum Text Size**: 14px. 119 | - **Maximum Heading Size**: 48px. 120 | - **Line Length**: 65-75 characters. 121 | 122 | ### 5. Visual Hierarchy 123 | 124 | - **Size Relationships** 125 | - **Main Headings**: 32px-40px. 126 | - **Subheadings**: 24px-32px. 127 | - **Body Text**: 16px-18px. 128 | - **Supporting Text**: 14px-16px. 129 | - **Labels**: 12px-14px. 130 | 131 | - **Weight Distribution** 132 | - **Primary Content**: Font weight 700-800. 133 | - **Secondary Headings**: Font weight 600. 134 | - **Body Text**: Font weight 400-500. 135 | - **Supporting Text**: Font weight 400. 136 | - **Emphasis**: Minimum 200 weight difference. 137 | 138 | ### 6. Depth and Visual Character 139 | 140 | - **Component Personality** 141 | - Use card elements for visual anchoring. 142 | - Highlight key components as visual focal points. 143 | - Apply subtle depth changes to interactive elements. 144 | - Make feature components larger than supporting elements. 145 | - Limit accent elements to 10% of the interface. 146 | 147 | - **Depth Hierarchy** 148 | - **Primary Cards**: Use medium elevation shadows. 149 | - **Interactive Elements**: Change shadow on hover. 150 | - **Modal Dialogs**: Highest elevation. 151 | - **Nested Components**: Decrease shadow intensity progressively. 152 | - **Active States**: Reduce shadow for feedback. 153 | 154 | - **Visual Interest Points** 155 | - Make key data visualizations prominent. 156 | - Increase important numbers by 20%-30% over context. 157 | - Use accent colors only for critical information. 158 | - Follow platform conventions for interactive patterns. 159 | - Design status indicators with clear visual distinctions. 160 | 161 | - **Shadow Scales** 162 | - **Subtle Elevation**: 0 2px 4px rgba(0, 0, 0, 0.1). 163 | - **Medium Elevation**: 0 4px 6px rgba(0, 0, 0, 0.1). 164 | - **High Elevation**: 0 8px 16px rgba(0, 0, 0, 0.1). 165 | - **Inner Shadow**: Inset 0 2px 4px rgba(0, 0, 0, 0.05). 166 | - **Focus States**: 0 0 0 3px rgba(accent color, 0.4). 167 | 168 | - **Visual Interest** 169 | - **Transition Timing**: 150ms-300ms. 170 | - **Scale Changes**: 1.02-1.05 on hover. 171 | - **Opacity Variations**: 0.8-0.9 for inactive elements. 172 | - **Border Accents**: 2px-4px. 173 | - **Gradient Angles**: 45°, 90°, 180°. 174 | 175 | ### 7. Typography and Readability 176 | 177 | - **Text Properties** 178 | - **Body Line Height**: 1.5-1.6. 179 | - **Heading Line Height**: 1.2-1.3. 180 | - **Paragraph Spacing**: 1.5em. 181 | - **Letter Spacing**: -0.02em for headings. 182 | - **Word Spacing**: 0.05em for body text. 183 | 184 | - **Content Structure** 185 | - **Maximum Width**: 680px for body text. 186 | - **List Indentation**: 16px-24px. 187 | - **Quote Padding**: 24px-32px. 188 | - **Code Block Padding**: 16px. 189 | - **Caption Size**: 85%-90% of body text size. 190 | 191 | ### 8. Responsive Design 192 | 193 | - **Breakpoints** 194 | - **Mobile**: 320px-480px. 195 | - **Tablet**: 481px-768px. 196 | - **Desktop**: 769px-1024px. 197 | - **Large Screens**: 1025px-1200px. 198 | - **Extra Large**: 1201px and above. 199 | 200 | - **Scaling Factors** 201 | - **Typography Scale**: Increase 15%-20% between breakpoints. 202 | - **Spacing Increase**: 25%-50% from mobile to desktop. 203 | - **Container Padding**: 16px-32px based on viewport. 204 | - **Grid Columns**: 1-4 depending on width. 205 | - **Image Scaling**: Reduce images by 25%-33% for mobile. 206 | 207 | - **Responsive Adaptations** 208 | - Transition from grid to stack layouts. 209 | - Switch from multi-column to single-column layouts. 210 | - Convert navigation menus to hamburger menus on mobile. 211 | - Change horizontal lists to vertical lists. 212 | - Adapt desktop hover states to mobile press states. 213 | 214 | ### 9. Component Behavior Patterns 215 | 216 | - **Interactive States** 217 | - **Buttons**: Scale up by 1.02-1.05 on hover. 218 | - **Cards**: Elevate by 2px-4px when interacted with. 219 | - **Form Fields**: Highlight with 2px borders on focus. 220 | - **Click Feedback**: Respond within 50ms-100ms. 221 | - **Disabled States**: Set opacity to 50%-60%. 222 | 223 | - **State Transitions** 224 | - **Element Scaling**: 200ms-300ms. 225 | - **Color Changes**: 150ms-200ms. 226 | - **Position Shifts**: 300ms-400ms. 227 | - **Opacity Changes**: 200ms-250ms. 228 | - **Size Animations**: 250ms-350ms for height/width changes. 229 | 230 | ### 10. Performance Considerations 231 | 232 | 233 | - **Loading States** 234 | - **Skeleton Width**: Use 60%-80% of actual content width. 235 | - **Loading Indicator Size**: 24px-32px. 236 | - **Minimum Loading Time**: Display indicators if loading exceeds 300ms. 237 | - **Maximum Loading Time**: Provide feedback if loading exceeds 3 seconds. 238 | - **Progress Indicators**: Update every 500ms for long processes. 239 | 240 | ### 11. Microinteractions and Feedback 241 | 242 | - **Delightful Details** 243 | - Use subtle animations to enhance feedback. 244 | - Follow natural easing functions for animations. 245 | - Keep animation durations between 150ms-300ms. 246 | - Ensure microinteractions serve a functional purpose. 247 | 248 | - **Feedback Loops** 249 | - Provide immediate visual feedback for user actions. 250 | - Use auditory feedback judiciously and allow users to mute. 251 | 252 | ### 12. Error Handling and Validation 253 | 254 | - **Prevent Errors** 255 | - Implement input constraints and smart defaults. 256 | - Use real-time validation to prevent errors. 257 | 258 | - **Clear Error Messages** 259 | - Explain what went wrong and how to fix it. 260 | - Place error messages near the relevant input field. 261 | 262 | ### 13. Consistency and Standards 263 | 264 | - **Design Systems** 265 | - Develop a style guide or design system for consistency. 266 | - Use reusable components throughout the application. 267 | 268 | - **Platform Conventions** 269 | - Adhere to design guidelines specific to iOS, Android, or web. 270 | 271 | ### 14. Emotional Design 272 | 273 | - **Aesthetic Appeal** 274 | - Balance functionality with visual appeal. 275 | - Use visuals and interactions to create an emotional connection. 276 | 277 | - **Brand Alignment** 278 | - Ensure the design reflects the brand's identity and values. 279 | 280 | --- 281 | 282 | By adhering to these comprehensive design principles, you can create user interfaces that are visually appealing, intuitive, and user-friendly. Keep the focus on design elements that enhance usability and aesthetics, ensuring a seamless experience for users across various devices and platforms. -------------------------------------------------------------------------------- /app/pages/index.vue: -------------------------------------------------------------------------------- 1 | import { ClientOnly } from '../../.nuxt/components'; 2 | 288 | 289 | 472 | 517 | --------------------------------------------------------------------------------