├── .gitattributes
├── .gitignore
├── README.md
├── cmd
└── fingerprint
│ └── main.go
├── dist
├── script.min.wasm
├── script.wasm
└── script.wat
├── go.mod
├── go.work
├── internal
├── canvas
│ ├── canvasfp.go
│ └── picasso.go
├── crypto
│ ├── hex.go
│ └── murmur.go
├── protobuf
│ ├── proto.go
│ └── proto_test.go
├── types
│ └── default.go
└── webgl
│ ├── engine.go
│ ├── fingerprint.go
│ └── params.go
├── scripts
└── Makefile
└── test
├── serve.go
└── static
├── index.html
└── js
├── gzip.min.js
└── init.min.js
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Wasm Web Fingerprinting library
2 | Js/Wasm Obfuscated fingerprinting, bot detection & API protection library
3 |
4 | state: `Pre-alpha`
5 |
6 | ### To implement:
7 | - [x] Canvas Fp
8 | - [x] Implement ProtoBuf protocol for communication
9 | - [ ] If not switching to another languge/compiling method, optimise wasm loading and glue code
10 | - [ ] Advanced Canvas Fp
11 | - [x] WebGl Fp & Params Fp
12 | - [ ] Screen Fp / Browser properties
13 | - [ ] Audio Fp
14 | - [ ] Css / Js and other fp techniques
15 | - [ ] Bot / Automation detection
16 | - [ ] Use mouse movements & bezier
17 | - [ ] Tls and Ja3 Fingerprinting
18 | - [ ] Make a Browser fp (finegrained) and Device fp (large grained, targets device)
19 | - [ ] Implement Obfuscation (although wasm is a first step) and Encryption
20 | - [ ] Implement an api that gets the fingerprint and processes data
21 | - [ ] Implement all fp's natively (without go) to increase speed (rust ?)
22 | - [ ] Train a model on recognising bad fp's
23 | - [ ] Implement techniques to make fp as authentic as possible and difficult to fake
24 |
25 | ### Compiling golang to wasm
26 | navigate to `./scripts` and run:
27 | ```sh
28 | make
29 | ```
30 |
31 |
32 |
33 | ### Running the script
34 | navigate to `./test` and run:
35 | ```sh
36 | go run serve.go
37 | ```
38 |
39 | You can then open [localhost:8080](http://localhost:8080) and the Fingerprint will be logged to console
40 | The fingerprints can be Accessible through calling `getFp()`
41 |
42 |
43 |
44 | ### Performance
45 | - Fp takes `~ 15ms` to compute (on `apple m2 air`)
46 | - Wasm size is `80kb` and `28kb`, before and after compression
47 |
48 | ### Optimizing compiled wasm from golang (both tinygo and gzip are used here)
49 |
50 | - using [tinygo](https://github.com/tinygo-org/tinygo) ~ `75%` filesize reduction
51 | ```js
52 | // using -no-debug and -opt=z to strip debug info and minimize filesize
53 | tinygo build -o output.wasm -target wasm -no-debug -opt=z input.go
54 | ```
55 | drawback: limited library implementation - solution: implement libraries natively like [`HexEncode`](https://github.com/onlpsec/fingerprint/blob/main/internal/crypto/hex.go).
56 |
57 | - using [gzip](https://www.gnu.org/software/gzip/) ~ `50%` filesize reduction
58 | ```sh
59 | gzip -9 -v -c input.wasm > output.min.wasm
60 | ```
61 | drawbacks: + `21kb` from [gzip](https://github.com/onlpsec/fingerprint/blob/main/test/static/gzip.min.js) javascript library
62 |
63 | VsCode settings (for gopls):
64 | ```sh
65 | GOOS=js GOARCH=wasm
66 | ```
67 | - if not, you will get an annoying (fake) error for including `syscall/js`
68 |
69 | ### Credits
70 | - https://newassets.hcaptcha.com/c/ac578c1/hsw.js
71 | - https://github.com/fingerprintjs/fingerprintjs
72 |
--------------------------------------------------------------------------------
/cmd/fingerprint/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "syscall/js"
5 | "time"
6 |
7 | "github.com/onlpsec/fingerprint/internal/canvas"
8 | "github.com/onlpsec/fingerprint/internal/crypto"
9 | "github.com/onlpsec/fingerprint/internal/protobuf"
10 | "github.com/onlpsec/fingerprint/internal/webgl"
11 | )
12 |
13 | func ProtoTest(this js.Value, inputs []js.Value) interface{} {
14 | hash, winding := canvas.CanvasFp()
15 |
16 | proto_bean := map[int]interface{}{
17 | 1: "field_ome",
18 | 2: 2,
19 | 3: map[int]interface{}{
20 | 1: hash,
21 | 2: winding,
22 | },
23 | 4: "abcdef",
24 | 5: "field_ome",
25 | 6: 2,
26 | 7: map[int]interface{}{
27 | 1: 1,
28 | 2: 2,
29 | 3: map[int]interface{}{
30 | 1: 1,
31 | 2: 2,
32 | },
33 | },
34 | 8: "abcdef",
35 | }
36 |
37 | return crypto.HexEncode(protobuf.ProtoBuf(proto_bean))
38 | }
39 |
40 | func getFp(this js.Value, inputs []js.Value) interface{} {
41 | start := time.Now()
42 | canvas_fp, canvas_winding := canvas.CanvasFp()
43 | webgl_fp := webgl.WebglFp()
44 | webgl_params := webgl.GetWebGLParameters()
45 |
46 | performance := time.Since(start).Seconds()
47 |
48 | return js.ValueOf(map[string]interface{}{
49 | "canvas": map[string]interface{}{
50 | "fp": canvas_fp,
51 | "winding": canvas_winding,
52 | },
53 | "webgl": map[string]interface{}{
54 | "fp": webgl_fp,
55 | "params": map[string]interface{}{
56 | "extensions": webgl_params.Extensions,
57 | "general": webgl_params.General,
58 | "shaderprecision": webgl_params.ShaderPrecision,
59 | },
60 | },
61 | "elapsed": performance,
62 | })
63 | }
64 |
65 | func main() {
66 | c := make(chan struct{}, 0)
67 |
68 | js.Global().Set("ProtoTest", js.FuncOf(ProtoTest))
69 | js.Global().Set("getFp", js.FuncOf(getFp))
70 | <-c
71 | }
72 |
--------------------------------------------------------------------------------
/dist/script.min.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onlpsec/fingerprint/19371a9f83d1aad0bd147efa3a0f7b9d9d9d5c97/dist/script.min.wasm
--------------------------------------------------------------------------------
/dist/script.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onlpsec/fingerprint/19371a9f83d1aad0bd147efa3a0f7b9d9d9d5c97/dist/script.wasm
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/onlpsec/fingerprint
2 |
3 | go 1.21.4
--------------------------------------------------------------------------------
/go.work:
--------------------------------------------------------------------------------
1 | go 1.21.4
2 |
3 | use (
4 | ./
5 | )
--------------------------------------------------------------------------------
/internal/canvas/canvasfp.go:
--------------------------------------------------------------------------------
1 | package canvas
2 |
3 | import (
4 | "syscall/js"
5 |
6 | "github.com/onlpsec/fingerprint/internal/crypto"
7 | )
8 |
9 | func drawCircle(ctx js.Value, x, y, radius int) {
10 | Pi := 3.14159265358979323846264338327950288419716939937510582097494459
11 | ctx.Call("beginPath")
12 | ctx.Call("arc", x, y, radius, 0, Pi*2, true)
13 | ctx.Call("closePath")
14 | ctx.Call("fill")
15 | }
16 |
17 | func CanvasFp() (string, int) {
18 |
19 | canvas := js.Global().Get("document").Call("createElement", "canvas")
20 | canvas.Set("width", 2000)
21 | canvas.Set("height", 200)
22 | canvas.Get("style").Set("display", "inline")
23 |
24 | ctx := canvas.Call("getContext", "2d")
25 | ctx.Call("rect", 0, 0, 10, 10)
26 | ctx.Call("rect", 2, 2, 6, 6)
27 |
28 | has_winding := 0
29 | winding := ctx.Call("isPointInPath", 5, 5, "evenodd").Bool() == false
30 | if winding == true {
31 | has_winding = 1
32 | }
33 |
34 | ctx.Set("textBaseline", "alphabetic")
35 | ctx.Set("fillStyle", "#f60")
36 | ctx.Call("fillRect", 125, 1, 62, 20)
37 | ctx.Set("fillStyle", "#069")
38 | ctx.Set("font", "11pt Arial")
39 | ctx.Call("fillText", "Cwm fjordbank glyphs vext quiz, 😂😂", 2, 15)
40 |
41 | ctx.Set("fillStyle", "rgba(102, 204, 0, 0.2)")
42 | ctx.Set("font", "18pt Arial")
43 | ctx.Call("fillText", "Cwm fjordbank glyphs vext quiz, 😂😂", 4, 45)
44 |
45 | ctx.Set("globalCompositeOperation", "multiply")
46 | ctx.Set("fillStyle", "rgb(255,0,255)")
47 | drawCircle(ctx, 50, 50, 50)
48 | ctx.Set("fillStyle", "rgb(0,255,255)")
49 | drawCircle(ctx, 100, 50, 50)
50 | ctx.Set("fillStyle", "rgb(255,255,0)")
51 | drawCircle(ctx, 75, 100, 50)
52 | ctx.Set("fillStyle", "rgb(255,0,255)")
53 |
54 | drawCircle(ctx, 75, 75, 75)
55 | drawCircle(ctx, 75, 75, 25)
56 | ctx.Call("fill", "evenodd")
57 |
58 | hash := ""
59 | if canvas.Get("toDataURL").Truthy() {
60 | // x64hash128 unavailable in GO
61 | hash = crypto.X64hash128(canvas.Call("toDataURL").String())
62 | } else {
63 | return "", -1
64 | }
65 |
66 | return hash, has_winding
67 | }
68 |
--------------------------------------------------------------------------------
/internal/canvas/picasso.go:
--------------------------------------------------------------------------------
1 | package canvas
2 |
3 | import (
4 | "github.com/onlpsec/fingerprint/internal/crypto"
5 | )
6 |
7 | func PicassoCanvasFp(roundNumber, params int) string {
8 |
9 | return crypto.X64hash128("")
10 | }
11 |
--------------------------------------------------------------------------------
/internal/crypto/hex.go:
--------------------------------------------------------------------------------
1 | package crypto
2 |
3 | const hextable = "0123456789abcdef"
4 |
5 | func EncodedLen(n int) int { return n * 2 }
6 |
7 | func Encode(dst, src []byte) int {
8 | j := 0
9 | for _, v := range src {
10 | dst[j] = hextable[v>>4]
11 | dst[j+1] = hextable[v&0x0f]
12 | j += 2
13 | }
14 | return len(src) * 2
15 | }
16 |
17 | func HexEncode(src []byte) string {
18 | dst := make([]byte, EncodedLen(len(src)))
19 | Encode(dst, src)
20 | return string(dst)
21 | }
22 |
--------------------------------------------------------------------------------
/internal/crypto/murmur.go:
--------------------------------------------------------------------------------
1 | package crypto
2 |
3 | const (
4 | c1 uint64 = 0x87c37b91114253d5
5 | c2 uint64 = 0x4cf5ad432745937f
6 | )
7 |
8 | type Hash interface {
9 | Write([]byte) (int, error)
10 | Sum([]byte) []byte
11 | Reset()
12 | Size() int
13 | BlockSize() int
14 | }
15 |
16 | type (
17 | sum128 struct {
18 | digest [2]uint64
19 | seed uint64
20 | }
21 |
22 | Hash128 interface {
23 | Hash
24 | Sum128() [2]uint64
25 | }
26 | )
27 |
28 | func rotl64(n uint64, count uint) uint64 {
29 | return (n << count) | (n >> (64 - count))
30 | }
31 |
32 | func New128() Hash128 {
33 | return New128WithSeed(0)
34 | }
35 |
36 | func New128WithSeed(seed uint64) Hash128 {
37 | return &sum128{digest: [2]uint64{seed, seed}, seed: seed}
38 | }
39 |
40 | func (s *sum128) Sum128() [2]uint64 {
41 | return s.digest
42 | }
43 |
44 | func (s *sum128) Write(data []byte) (int, error) {
45 | tail := s.blockInterMix(data)
46 | s.blockInterMixTail(tail)
47 | s.finalizeMix(data)
48 |
49 | return len(data), nil
50 | }
51 |
52 | func (s *sum128) blockInterMix(data []byte) []byte {
53 | tailStart := 0
54 | for i := 0; i+s.Size() <= len(data); i += s.Size() {
55 | var k1, k2 uint64
56 |
57 | tailStart += s.Size()
58 |
59 | x := data[i : i+s.Size()/2]
60 | y := data[i+s.Size()/2:]
61 |
62 | for j := 0; j < s.Size()/2; j++ {
63 | k1 |= uint64(x[j]) << uint(j*8)
64 | k2 |= uint64(y[j]) << uint(j*8)
65 | }
66 |
67 | k1 *= c1
68 | k1 = rotl64(k1, 31)
69 | k1 *= c2
70 |
71 | s.digest[0] ^= k1
72 | s.digest[0] = rotl64(s.digest[0], 27)
73 | s.digest[0] += s.digest[1]
74 | s.digest[0] = s.digest[0]*5 + 0x52dce729
75 |
76 | k2 *= c2
77 | k2 = rotl64(k2, 33)
78 | k2 *= c1
79 |
80 | s.digest[1] ^= k2
81 | s.digest[1] = rotl64(s.digest[1], 31)
82 | s.digest[1] += s.digest[0]
83 | s.digest[1] = s.digest[1]*5 + 0x38495ab5
84 | }
85 |
86 | return data[tailStart:]
87 | }
88 |
89 | func (s *sum128) blockInterMixTail(tail []byte) {
90 | var k1, k2 uint64
91 |
92 | switch len(tail) & 15 {
93 | case 15:
94 | k2 ^= uint64(tail[14]) << 48
95 | fallthrough
96 | case 14:
97 | k2 ^= uint64(tail[13]) << 40
98 | fallthrough
99 | case 13:
100 | k2 ^= uint64(tail[12]) << 32
101 | fallthrough
102 | case 12:
103 | k2 ^= uint64(tail[11]) << 24
104 | fallthrough
105 | case 11:
106 | k2 ^= uint64(tail[10]) << 16
107 | fallthrough
108 | case 10:
109 | k2 ^= uint64(tail[9]) << 8
110 | fallthrough
111 | case 9:
112 | k2 ^= uint64(tail[8]) << 0
113 | k2 *= c2
114 | k2 = rotl64(k2, 33)
115 | k2 *= c1
116 |
117 | s.digest[1] ^= k2
118 | fallthrough
119 | case 8:
120 | k1 ^= uint64(tail[7]) << 56
121 | fallthrough
122 | case 7:
123 | k1 ^= uint64(tail[6]) << 48
124 | fallthrough
125 | case 6:
126 | k1 ^= uint64(tail[5]) << 40
127 | fallthrough
128 | case 5:
129 | k1 ^= uint64(tail[4]) << 32
130 | fallthrough
131 | case 4:
132 | k1 ^= uint64(tail[3]) << 24
133 | fallthrough
134 | case 3:
135 | k1 ^= uint64(tail[2]) << 16
136 | fallthrough
137 | case 2:
138 | k1 ^= uint64(tail[1]) << 8
139 | fallthrough
140 | case 1:
141 | k1 ^= uint64(tail[0]) << 0
142 | k1 *= c1
143 | k1 = rotl64(k1, 31)
144 | k1 *= c2
145 |
146 | s.digest[0] ^= k1
147 | }
148 | }
149 |
150 | func (s *sum128) finalizeMix(data []byte) {
151 | s.digest[0] ^= uint64(len(data))
152 | s.digest[1] ^= uint64(len(data))
153 |
154 | s.digest[0] += s.digest[1]
155 | s.digest[1] += s.digest[0]
156 |
157 | s.digest[0] ^= s.digest[0] >> 33
158 | s.digest[0] *= 0xff51afd7ed558ccd
159 | s.digest[0] ^= s.digest[0] >> 33
160 | s.digest[0] *= 0xc4ceb9fe1a85ec53
161 | s.digest[0] ^= s.digest[0] >> 33
162 |
163 | s.digest[1] ^= s.digest[1] >> 33
164 | s.digest[1] *= 0xff51afd7ed558ccd
165 | s.digest[1] ^= s.digest[1] >> 33
166 | s.digest[1] *= 0xc4ceb9fe1a85ec53
167 | s.digest[1] ^= s.digest[1] >> 33
168 |
169 | s.digest[0] += s.digest[1]
170 | s.digest[1] += s.digest[0]
171 |
172 | }
173 |
174 | func (s *sum128) Sum(in []byte) []byte {
175 | return append(in,
176 | byte(s.digest[0]>>56),
177 | byte(s.digest[0]>>48),
178 | byte(s.digest[0]>>40),
179 | byte(s.digest[0]>>32),
180 | byte(s.digest[0]>>24),
181 | byte(s.digest[0]>>16),
182 | byte(s.digest[0]>>8),
183 | byte(s.digest[0]),
184 | byte(s.digest[1]>>56),
185 | byte(s.digest[1]>>48),
186 | byte(s.digest[1]>>40),
187 | byte(s.digest[1]>>32),
188 | byte(s.digest[1]>>24),
189 | byte(s.digest[1]>>16),
190 | byte(s.digest[1]>>8),
191 | byte(s.digest[1]),
192 | )
193 |
194 | }
195 |
196 | func (s *sum128) Reset() {
197 | s.digest[0] = s.seed
198 | s.digest[1] = s.seed
199 | }
200 |
201 | func (s *sum128) Size() int {
202 | return 16
203 | }
204 |
205 | func (s *sum128) BlockSize() int {
206 | return 1
207 | }
208 |
209 | func X64hash128(content string) string {
210 | hash := New128WithSeed(1234)
211 | data := []byte(content)
212 | hash.Write(data)
213 | hashedValue := hash.Sum(nil)
214 |
215 | hexStr := make([]byte, EncodedLen(len(hashedValue)))
216 | Encode(hexStr, hashedValue)
217 |
218 | return string(hexStr)
219 | }
220 |
--------------------------------------------------------------------------------
/internal/protobuf/proto.go:
--------------------------------------------------------------------------------
1 | package protobuf
2 |
3 | import (
4 | "bytes"
5 | "sort"
6 | )
7 |
8 | type ProtoFieldType int
9 |
10 | const (
11 | VARINT ProtoFieldType = iota
12 | INT64
13 | STRING
14 | )
15 |
16 | func write0(data *bytes.Buffer, byte byte) {
17 | data.WriteByte(byte & 0xFF)
18 | }
19 |
20 | func write(data *bytes.Buffer, bytes []byte) {
21 | data.Write(bytes)
22 | }
23 |
24 | func writeVarint(data *bytes.Buffer, vint uint32) {
25 | vint = vint & 0xFFFFFFFF
26 | for vint > 0x80 {
27 | write0(data, byte((vint&0x7F)|0x80))
28 | vint >>= 7
29 | }
30 | write0(data, byte(vint&0x7F))
31 | }
32 |
33 | func writeString(data *bytes.Buffer, str string) {
34 | writeVarint(data, uint32(len(str)))
35 | write(data, []byte(str))
36 | }
37 |
38 | func ProtoBuf(data map[int]interface{}) []byte {
39 | result := new(bytes.Buffer)
40 |
41 | // Get the keys and sort them
42 | keys := make([]int, 0, len(data))
43 | for k := range data {
44 | keys = append(keys, k)
45 | }
46 | sort.Ints(keys)
47 |
48 | // Iterate over the sorted keys
49 | for _, k := range keys {
50 | v := data[k]
51 | switch v := v.(type) {
52 | case int:
53 | key := (k << 3) | int(VARINT&7)
54 | writeVarint(result, uint32(key))
55 | writeVarint(result, uint32(v))
56 |
57 | case string:
58 | key := (k << 3) | int(STRING&7)
59 | writeVarint(result, uint32(key))
60 | writeString(result, v)
61 |
62 | case map[int]interface{}:
63 | key := (k << 3) | int(STRING&7)
64 | writeVarint(result, uint32(key))
65 | writeString(result, string(ProtoBuf(v)))
66 |
67 | default:
68 | panic("")
69 | }
70 | }
71 | return result.Bytes()
72 | }
73 |
--------------------------------------------------------------------------------
/internal/protobuf/proto_test.go:
--------------------------------------------------------------------------------
1 | package protobuf_test
2 |
--------------------------------------------------------------------------------
/internal/types/default.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type FpStruct struct {
4 | Hash string
5 | Winding int
6 | }
7 |
--------------------------------------------------------------------------------
/internal/webgl/engine.go:
--------------------------------------------------------------------------------
1 | package webgl
2 |
3 | import "syscall/js"
4 |
5 | func getWebglEngine() js.Value {
6 | doc := js.Global().Get("document")
7 | canvas := doc.Call("createElement", "canvas")
8 | var gl js.Value
9 | defer func() {
10 | if r := recover(); r != nil {
11 | gl = js.Null()
12 | }
13 | }()
14 | gl = canvas.Call("getContext", "webgl")
15 | if gl.IsNull() {
16 | gl = canvas.Call("getContext", "experimental-webgl")
17 | }
18 | return gl
19 | }
20 |
--------------------------------------------------------------------------------
/internal/webgl/fingerprint.go:
--------------------------------------------------------------------------------
1 | package webgl
2 |
3 | import (
4 | "syscall/js"
5 |
6 | "github.com/onlpsec/fingerprint/internal/crypto"
7 | )
8 |
9 | func WebglFp() interface{} {
10 | webglContext := getWebglEngine()
11 | if webglContext.IsNull() {
12 | return -1
13 | }
14 |
15 | vShaderTemplate := "attribute vec2 attrVertex;varying vec2 varyinTexCoordinate;uniform vec2 uniformOffset;void main(){varyinTexCoordinate=attrVertex+uniformOffset;gl_Position=vec4(attrVertex,0,1);}"
16 | fShaderTemplate := "precision mediump float;varying vec2 varyinTexCoordinate;void main() {gl_FragColor=vec4(varyinTexCoordinate,0,1);}"
17 |
18 | vertexPosBuffer := webglContext.Call("createBuffer")
19 | webglContext.Call("bindBuffer", webglContext.Get("ARRAY_BUFFER"), vertexPosBuffer)
20 |
21 | vertices := []float32{-0.2, -0.9, 0, 0.4, -0.26, 0, 0, 0.732134444, 0}
22 | array := make([]interface{}, len(vertices))
23 | for i, v := range vertices {
24 | array[i] = v
25 | }
26 | jsVertices := js.ValueOf(array)
27 | float32Array := js.Global().Get("Float32Array").New(jsVertices)
28 | webglContext.Call("bufferData", webglContext.Get("ARRAY_BUFFER"), float32Array, webglContext.Get("STATIC_DRAW"))
29 |
30 | program := webglContext.Call("createProgram")
31 | vshader := webglContext.Call("createShader", webglContext.Get("VERTEX_SHADER"))
32 | webglContext.Call("shaderSource", vshader, vShaderTemplate)
33 | webglContext.Call("compileShader", vshader)
34 | fshader := webglContext.Call("createShader", webglContext.Get("FRAGMENT_SHADER"))
35 | webglContext.Call("shaderSource", fshader, fShaderTemplate)
36 | webglContext.Call("compileShader", fshader)
37 |
38 | webglContext.Call("attachShader", program, vshader)
39 | webglContext.Call("attachShader", program, fshader)
40 | webglContext.Call("linkProgram", program)
41 | webglContext.Call("useProgram", program)
42 |
43 | attrVertex := webglContext.Call("getAttribLocation", program, "attrVertex")
44 | webglContext.Call("enableVertexAttribArray", attrVertex)
45 | webglContext.Call("vertexAttribPointer", attrVertex, 2, webglContext.Get("FLOAT"), false, 0, 0)
46 |
47 | uniformOffset := webglContext.Call("getUniformLocation", program, "uniformOffset")
48 | webglContext.Call("uniform2f", uniformOffset, 1, 1)
49 |
50 | webglContext.Call("drawArrays", webglContext.Get("TRIANGLE_STRIP"), 0, len(vertices)/2)
51 |
52 | result := webglContext.Get("canvas").Call("toDataURL").String()
53 |
54 | return crypto.X64hash128(result)
55 | }
56 |
--------------------------------------------------------------------------------
/internal/webgl/params.go:
--------------------------------------------------------------------------------
1 | package webgl
2 |
3 | import (
4 | "syscall/js"
5 | )
6 |
7 | type WebGLParameters struct {
8 | Extensions []interface{}
9 | General map[string]interface{}
10 | ShaderPrecision map[string]interface{}
11 | }
12 |
13 | func GetWebGLParameters() WebGLParameters {
14 |
15 | glContext := getWebglEngine()
16 |
17 | maxAnisotropy := func(e js.Value) js.Value {
18 | t := js.Value{}
19 | i := e.Call("getExtension", "EXT_texture_filter_anisotropic")
20 | if !i.Truthy() {
21 | i = e.Call("getExtension", "WEBKIT_EXT_texture_filter_anisotropic")
22 | }
23 | if !i.Truthy() {
24 | i = e.Call("getExtension", "MOZ_EXT_texture_filter_anisotropic")
25 | }
26 |
27 | if i.Truthy() {
28 | t = e.Call("getParameter", i.Get("MAX_TEXTURE_MAX_ANISOTROPY_EXT"))
29 | if t.Int() == 0 {
30 | t = js.ValueOf(2)
31 | }
32 | }
33 |
34 | return t
35 | }(glContext)
36 |
37 | webglParameters := map[string]interface{}{
38 | "MAX_ANISOTROPY": maxAnisotropy,
39 | "ANTIALIAS": "no",
40 | }
41 | if glContext.Call("getContextAttributes").Get("antialias").Truthy() {
42 | webglParameters["ANTIALIAS"] = "yes"
43 | }
44 |
45 | parameters := []string{
46 | "ALPHA_BITS",
47 | "BLUE_BITS",
48 | "DEPTH_BITS",
49 | "GREEN_BITS",
50 | "MAX_COMBINED_TEXTURE_IMAGE_UNITS",
51 | "MAX_CUBE_MAP_TEXTURE_SIZE",
52 | "MAX_FRAGMENT_UNIFORM_VECTORS",
53 | "MAX_RENDERBUFFER_SIZE",
54 | "MAX_TEXTURE_IMAGE_UNITS",
55 | "MAX_TEXTURE_SIZE",
56 | "MAX_VARYING_VECTORS",
57 | "MAX_VERTEX_ATTRIBS",
58 | "MAX_VERTEX_TEXTURE_IMAGE_UNITS",
59 | "MAX_VERTEX_UNIFORM_VECTORS",
60 | "RED_BITS",
61 | "RENDERER",
62 | "SHADING_LANGUAGE_VERSION",
63 | "STENCIL_BITS",
64 | "VENDOR",
65 | "VERSION",
66 | }
67 |
68 | arrayParams := []string{
69 | "ALIASED_LINE_WIDTH_RANGE",
70 | "ALIASED_POINT_SIZE_RANGE",
71 | "MAX_VIEWPORT_DIMS",
72 | }
73 |
74 | shaders := []string{
75 | "VERTEX_SHADER",
76 | "FRAGMENT_SHADER",
77 | }
78 |
79 | precisions := []string{
80 | "HIGH_FLOAT",
81 | "MEDIUM_FLOAT",
82 | "LOW_FLOAT",
83 | "HIGH_INT",
84 | "MEDIUM_INT",
85 | "LOW_INT",
86 | }
87 |
88 | precisionAttributesKeys := []string{"rangeMin", "rangeMax", "precision"}
89 |
90 | precisionAttributes := map[string]interface{}{}
91 |
92 | for _, shader := range shaders {
93 | for _, precision := range precisions {
94 | for _, attrKey := range precisionAttributesKeys {
95 | precisionAttributes[shader+"_"+precision+"_"+attrKey] =
96 | glContext.Call("getShaderPrecisionFormat", glContext.Get(shader), glContext.Get(precision)).Get(attrKey)
97 | }
98 | }
99 | }
100 |
101 | for _, parameter := range parameters {
102 | webglParameters[parameter] = glContext.Call("getParameter", glContext.Get(parameter))
103 | }
104 |
105 | for _, parameter := range arrayParams {
106 | jsonParam := glContext.Call("getParameter", glContext.Get(parameter))
107 | webglParameters[parameter] = "[" + jsonParam.Index(0).String() + "," + jsonParam.Index(1).String() + "]"
108 | }
109 |
110 | webglExtensions := glContext.Call("getSupportedExtensions")
111 |
112 | extensions := make([]interface{}, webglExtensions.Length())
113 | for i := 0; i < webglExtensions.Length(); i++ {
114 | extensions[i] = webglExtensions.Index(i).String()
115 | }
116 |
117 | return WebGLParameters{
118 | Extensions: extensions,
119 | General: webglParameters,
120 | ShaderPrecision: precisionAttributes,
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/scripts/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: compile
2 |
3 | compile:
4 | @echo "compiling to wasm ..."
5 | cd ../cmd/fingerprint/ && tinygo build -o ../../dist/script.wasm -target wasm -no-debug -opt=z main.go
6 | @size=$$(($$(wc -c < ../dist/script.wasm)/1024)) ; \
7 | printf "wasm size: %s kB\n" $$size
8 | @echo "compressing wasm with gzip..."
9 | gzip -9 -v -c ../dist/script.wasm > ../dist/script.min.wasm
10 | @size=$$(($$(wc -c < ../dist/script.min.wasm)/1024)) ; \
11 | printf "compressed wasm size: %s kB\n" $$size
12 | @echo "done. wasm is in ./dist/script.wasm"
--------------------------------------------------------------------------------
/test/serve.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | )
7 |
8 | func main() {
9 | fs := http.FileServer(http.Dir("./static"))
10 | http.Handle("/static/", http.StripPrefix("/static/", fs))
11 |
12 | fs1 := http.FileServer(http.Dir("./../dist"))
13 | http.Handle("/dist/", http.StripPrefix("/dist/", fs1))
14 |
15 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
16 | http.ServeFile(w, r, "./static/index.html")
17 | })
18 |
19 | log.Println("Listening on http://localhost:8080...")
20 | err := http.ListenAndServe(":8080", nil)
21 | if err != nil {
22 | log.Fatal(err)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/test/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
>>=p,w-=p),w<15&&(u+=R[i++]< l){e.msg="invalid distance too far back",Z.mode=a;break e}if(u>>>=p,w-=p,x>(p=r-o)){if((p=x-p)>f&&Z.sane){e.msg="invalid distance too far back",Z.mode=a;break e}if(y=0,E=h,0===c){if(y+=d-p,p