Author: Adam <git@apiote.xyz>
joe black
.gitignore | 9 decoder/go.mod | 8 decoder/go.sum | 16 decoder/joeblack.go | 134 +++ decoder/mkfile | 2 encoder/go.mod | 9 encoder/go.sum | 18 encoder/joeblack.go | 155 +++ encoder/mkfile | 2 index.html | 69 + mkfile | 19 shared/shared.go | 101 ++ shared/wordlist | 2048 +++++++++++++++++++++++++++++++++++++++++++++++ style.css | 196 ++++ wasm_exec.js | 554 ++++++++++++
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..b4d8e00d479a19cac176bc62cc34c094e0895d7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +decoder/joeblack.wasm +decoder/shared.go +decoder/wordlist + +encoder/joeblack +encoder/shared.go +encoder/wordlist + +secrets.dirty diff --git a/decoder/go.mod b/decoder/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..0e08798ba9da6086bc31ad76e619dcf335cadfd9 --- /dev/null +++ b/decoder/go.mod @@ -0,0 +1,8 @@ +module apiote.xyz/p/joeblack + +go 1.18 + +require ( + git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9 + github.com/codahale/sss v0.0.0-20160501174526-0cb9f6d3f7f1 +) diff --git a/decoder/go.sum b/decoder/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..86541e53c044711163f4149986736d62d089eb09 --- /dev/null +++ b/decoder/go.sum @@ -0,0 +1,16 @@ +git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw= +git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9 h1:Ahny8Ud1LjVMMAlt8utUFKhhxJtwBAualvsbc/Sk7cE= +git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9/go.mod h1:BVJwbDfVjCjoFiKrhkei6NdGcZYpkDkdyCdg1ukytRA= +github.com/codahale/sss v0.0.0-20160501174526-0cb9f6d3f7f1 h1:PJJtqFbZH8ZW9PtsfB+ALZKVPRiRwNbPrNe+gliLpGo= +github.com/codahale/sss v0.0.0-20160501174526-0cb9f6d3f7f1/go.mod h1:0Vm/twPonvi1fkJ3kW8TbuttPQ4EyspL1xHUVr1I3uU= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/decoder/joeblack.go b/decoder/joeblack.go new file mode 100644 index 0000000000000000000000000000000000000000..6072629ff187aec8225dee73201cded4e781f628 --- /dev/null +++ b/decoder/joeblack.go @@ -0,0 +1,134 @@ +package main + +import ( + "embed" + "encoding/base64" + "fmt" + "github.com/codahale/sss" + + "git.sr.ht/~sircmpwn/go-bare" + "syscall/js" +) + +var ( + //go:embed wordlist + wordlistFs embed.FS + sharesNum = 3 + wordlist map[string]int +) + +func main() { + wordlistFile, err := wordlistFs.Open("wordlist") + if err != nil { + panic("while opening wordlist: " + err.Error()) + } + defer wordlistFile.Close() + wordlist = map[string]int{} + word := "" + for i := 0; i < 2048; i++ { + fmt.Fscanf(wordlistFile, "%s", &word) + wordlist[word] = i + } + js.Global().Set("decodeSSS", js.FuncOf(decodeSSS)) + js.Global().Set("decodePaperKey", js.FuncOf(decodePaperKey)) + js.Global().Set("decryptPayload", js.FuncOf(decryptPayload)) + <-make(chan bool) +} + +// args:: shares :[sharesNum]string... +func decodeSSS(this js.Value, args []js.Value) any { + if len(args) != sharesNum { + return fmt.Sprintf("ERR: Invalid number of arguments given %d, expected %d\n", len(args), sharesNum) + } + shares := map[byte][]byte{} + for i := 0; i < sharesNum; i++ { + share := args[i].String() + shareBytes, err := fromWords(share, wordlist) + if err != nil { + return fmt.Sprintf("ERR: while decoding words: %v", err) + } + shareNum := shareBytes[len(shareBytes)-1] + shareBytes = shareBytes[:len(shareBytes)-1] + shares[shareNum] = shareBytes + } + return base64.StdEncoding.EncodeToString(sss.Combine(shares)) +} + +// args:: words :string +func decodePaperKey(this js.Value, args []js.Value) any { + if len(args) != 1 { + return fmt.Sprintf("ERR: Invalid number of arguments given %d, expected 1\n", len(args)) + } + share := args[0].String() + shareBytes, err := fromWords(share, wordlist) + if err != nil { + return fmt.Sprintf("ERR: while decoding words: %v", err) + } + return base64.StdEncoding.EncodeToString(shareBytes) +} + +// args:: wordsKey :base64, payload :base64[, isPaperKey :bool] +func decryptPayload(this js.Value, args []js.Value) any { + if len(args) != 2 && len(args) != 3 { + return fmt.Sprintf("ERR: Invalid number of arguments given %d, expected 2 or 3\n", len(args)) + } + isPaperKey := false + if len(args) == 3 { + isPaperKey = args[2].Bool() + } + wordsKey, err := base64.StdEncoding.DecodeString(args[0].String()) + if err != nil { + return fmt.Sprintf("ERR: while decoding base64 key: %v", err) + } + rawPayload, err := base64.StdEncoding.DecodeString(args[1].String()) + if err != nil { + return fmt.Sprintf("ERR: while decoding base64 payload: %v", err) + } + payload := Payload{} + payload.Keys = make([][]byte, 2) + l1 := rawPayload[0] + l2 := rawPayload[l1+1] + var key []byte + var keyNum int + if isPaperKey { + keyNum = 1 + } else { + keyNum = 0 + } + + payload.Keys[0] = rawPayload[1 : l1+1] + payload.Keys[1] = rawPayload[l1+2 : l1+2+l2] + key, err = decrypt(payload.Keys[keyNum], wordsKey) + if err != nil { + return fmt.Sprintf("ERR: while decrypting key: %v", err) + } + payload.Secrets = rawPayload[l1+2+l2:] + rawSecrets, err := decrypt(payload.Secrets, key) + if err != nil { + return fmt.Sprintf("ERR: while decrypting secrets: %v", err) + } + secrets := []Secret{} + err = bare.Unmarshal(rawSecrets, &secrets) + if err != nil { + return fmt.Sprintf("ERR: while unmarshalling secrets: %v", err) + } + + export := "" + secretsLen := len(secrets) + for i, secret := range secrets { + fields := []string{} + for key := range secret { + fields = append(fields, key) + } + for _, field := range fields { + if secret[field].Hidden { + export += fmt.Sprintf("[H] ") + } + export += fmt.Sprintf("%s: %s\n", field, secret[field].Value) + } + if i+1 < secretsLen { + export += fmt.Sprintln("---") + } + } + return export +} diff --git a/decoder/mkfile b/decoder/mkfile new file mode 100644 index 0000000000000000000000000000000000000000..288cfd7a6fd3b9e2a37fa3a91c92ccb49424f941 --- /dev/null +++ b/decoder/mkfile @@ -0,0 +1,2 @@ +joeblack.wasm: joeblack.go + GOOS=js GOARCH=wasm go build -o joeblack.wasm diff --git a/encoder/go.mod b/encoder/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..607f8859ae9efa34e685513a37ba1211ca42c6d2 --- /dev/null +++ b/encoder/go.mod @@ -0,0 +1,9 @@ +module apiote.xyz/p/joeblack + +go 1.18 + +require ( + apiote.xyz/p/go-dirty v0.0.0-20211218161334-e486e7b5cf43 + git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9 + github.com/codahale/sss v0.0.0-20160501174526-0cb9f6d3f7f1 +) diff --git a/encoder/go.sum b/encoder/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..457c1d5fdda9b86303af79967b915053ec3039b2 --- /dev/null +++ b/encoder/go.sum @@ -0,0 +1,18 @@ +apiote.xyz/p/go-dirty v0.0.0-20211218161334-e486e7b5cf43 h1:C6YDhXc8Wo1ZrhxeJB5BzoYRI3plju2tg0ShTvdEaTQ= +apiote.xyz/p/go-dirty v0.0.0-20211218161334-e486e7b5cf43/go.mod h1:8QnoYcdnf+1AmRhC7GBUKuCl/wRpfI3Bd58JqDbJNtw= +git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw= +git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9 h1:Ahny8Ud1LjVMMAlt8utUFKhhxJtwBAualvsbc/Sk7cE= +git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9/go.mod h1:BVJwbDfVjCjoFiKrhkei6NdGcZYpkDkdyCdg1ukytRA= +github.com/codahale/sss v0.0.0-20160501174526-0cb9f6d3f7f1 h1:PJJtqFbZH8ZW9PtsfB+ALZKVPRiRwNbPrNe+gliLpGo= +github.com/codahale/sss v0.0.0-20160501174526-0cb9f6d3f7f1/go.mod h1:0Vm/twPonvi1fkJ3kW8TbuttPQ4EyspL1xHUVr1I3uU= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/encoder/joeblack.go b/encoder/joeblack.go new file mode 100644 index 0000000000000000000000000000000000000000..3ffdc470a69202677962b0aec172da3e72b732a9 --- /dev/null +++ b/encoder/joeblack.go @@ -0,0 +1,155 @@ +package main + +import ( + "bufio" + "crypto/rand" + "encoding/base64" + "fmt" + "github.com/codahale/sss" + "io" + "os" + + "apiote.xyz/p/go-dirty" + "git.sr.ht/~sircmpwn/go-bare" +) + +func main() { + wordlistFile, err := os.Open("wordlist") + if err != nil { + fmt.Printf("while opening wordlist: %v\n", err) + os.Exit(1) + } + defer wordlistFile.Close() + + wordlist := make([]string, 2048) + wordlistRev := map[string]int{} + for i := 0; i < 2048; i++ { + fmt.Fscanf(wordlistFile, "%s", &wordlist[i]) + wordlistRev[wordlist[i]] = i + } + + recreate := false + if len(os.Args) > 1 && os.Args[1] == "-r" { + recreate = true + } + + payload := Payload{} + key := [32]byte{} + if !recreate { + _, err = io.ReadFull(rand.Reader, key[:]) + if err != nil { + fmt.Printf("while creating key: %v\n", err) + os.Exit(1) + } + } else { + paperKeyWords, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil { + fmt.Printf("while reading paper key: %v\n", err) + os.Exit(1) + } + paperKey, err := fromWords(paperKeyWords, wordlistRev) + if err != nil { + fmt.Printf("while decoding paper key: %v\n", err) + os.Exit(1) + } + payloadFile, err := os.Open("payload") + if err != nil { + fmt.Printf("while opening payload: %v\n", err) + os.Exit(1) + } + defer payloadFile.Close() + + payload.Keys = make([][]byte, 2) + l := make([]byte, 1) + _, err = io.ReadFull(payloadFile, l) + if err != nil { + fmt.Printf("while reading key[0] length: %v\n", err) + os.Exit(1) + } + payload.Keys[0] = make([]byte, l[0]) + _, err = io.ReadFull(payloadFile, payload.Keys[0]) + if err != nil { + fmt.Printf("while reading key[0]: %v\n", err) + os.Exit(1) + } + _, err = io.ReadFull(payloadFile, l) + if err != nil { + fmt.Printf("while reading key[1] length: %v\n", err) + os.Exit(1) + } + payload.Keys[1] = make([]byte, l[0]) + _, err = io.ReadFull(payloadFile, payload.Keys[1]) + if err != nil { + fmt.Printf("while reading key[1]: %v\n", err) + os.Exit(1) + } + k, err := decrypt(payload.Keys[1], paperKey) + if err != nil { + fmt.Printf("while decrypting paperKeyEnc: %v\n", err) + os.Exit(1) + } + copy(key[:], k) + } + + secrets := []Secret{} + eezeExportFile, err := os.Open("secrets.dirty") + if err != nil { + fmt.Printf("while opening secrets: %v\n", err) + os.Exit(1) + } + defer eezeExportFile.Close() + + err = dirty.LoadStruct(eezeExportFile, &secrets) + if err != nil { + fmt.Printf("while loading eeze export: %v\n", err) + os.Exit(1) + } + + bareSecrets, err := bare.Marshal(&secrets) + if err != nil { + fmt.Printf("while marshalling secrets: %v\n", err) + os.Exit(1) + } + + payload.Secrets, err = encrypt(bareSecrets, key[:]) + + if !recreate { + sssKey := [32]byte{} + _, err = io.ReadFull(rand.Reader, sssKey[:]) + if err != nil { + fmt.Printf("while creating sss key: %v\n", err) + os.Exit(1) + } + paperKey := [32]byte{} + _, err = io.ReadFull(rand.Reader, paperKey[:]) + if err != nil { + fmt.Printf("while creating paperkey: %v\n", err) + os.Exit(1) + } + sssKeyEnc, err := encrypt(key[:], sssKey[:]) + paperKeyEnc, err := encrypt(key[:], paperKey[:]) + payload.Keys = append(payload.Keys, sssKeyEnc) + payload.Keys = append(payload.Keys, paperKeyEnc) + + shares, err := sss.Split(5, 3, sssKey[:]) + if err != nil { + fmt.Printf("while spliting: %v\n", err) + os.Exit(1) + } + for i, share := range shares { + share = append(share, byte(i)) + fmt.Println(toWords(share, wordlist)) + } + fmt.Println("") + fmt.Println(toWords(paperKey[:], wordlist)) + } + + payloadBytes := []byte{} + payloadBytes = append(payloadBytes, byte(len(payload.Keys[0]))) + payloadBytes = append(payloadBytes, payload.Keys[0]...) + payloadBytes = append(payloadBytes, byte(len(payload.Keys[1]))) + payloadBytes = append(payloadBytes, payload.Keys[1]...) + payloadBytes = append(payloadBytes, payload.Secrets...) + payloadB64 := base64.StdEncoding.EncodeToString(payloadBytes) + os.WriteFile("payload", []byte(payloadB64), 0o644) +} diff --git a/encoder/mkfile b/encoder/mkfile new file mode 100644 index 0000000000000000000000000000000000000000..e5c77788355d98b6b20411cba6baa4646065901a --- /dev/null +++ b/encoder/mkfile @@ -0,0 +1,2 @@ +joeblack: joeblack.go + go build diff --git a/index.html b/index.html new file mode 100644 index 0000000000000000000000000000000000000000..8e4d011e1c61102ed2265d6b64719e65ea4aa810 --- /dev/null +++ b/index.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="stylesheet" href="style.css" /> + <title>Joe Black</title> + <script src="wasm_exec.js"></script> + <script> + const go = new Go(); + WebAssembly.instantiateStreaming(fetch('joeblack.wasm'), go.importObject).then((result) => { + go.run(result.instance); + }); + </script> + <script defer> + function process() { + const share1 = document.getElementById('share1'); + const share2 = document.getElementById('share2'); + const share3 = document.getElementById('share3'); + + let key = ''; + let isPaperKey = false; + if (share2.value == '' && share3.value == '') { + key = decodePaperKey(share1.value); + isPaperKey = true; + } else { + key = decodeSSS(share1.value, share2.value, share3.value) + } + if (key.startsWith('ERR: ')) { + alert(key); + return; + } + fetch('payload').then (payloadResponse => { + if (!payloadResponse.ok) { + alert("Nie można pobrać sejfu"); + return; + } + return payloadResponse.text(); + }).then(payload => { + let secrets = decryptPayload(key, payload, isPaperKey); + if (secrets.startsWith('ERR: ')) { + alert(secrets); + return; + } + let element = document.createElement('a'); + let blob = new Blob([secrets], {type: 'text/plain'}); + element.setAttribute('href', URL.createObjectURL(blob)); + element.setAttribute('download', 'secrets'); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + }); + } + </script> + </head> + <body> + <main> + <h1>Joe Black of mine</h1> + <label for="share1">Udział #1 (24 wyrazy)</label> + <textarea id="share1" style=" width: 100%"></textarea> + <label for="share2">Udział #2 (24 wyrazy)</label> + <textarea id="share2" style=" width: 100%"></textarea> + <label for="share3">Udział #3 (24 wyrazy)</label> + <textarea id="share3" style=" width: 100%"></textarea> + <button id="button" style="font-size: 1.5rem" onClick="process();">Dalej »</button> + </main> + </body> +</html> diff --git a/joeblack.wasm b/joeblack.wasm new file mode 100755 index 0000000000000000000000000000000000000000..c267e4163959892ab184b7582ae55a61ce14b638 Binary files /dev/null and b/joeblack.wasm differ diff --git a/mkfile b/mkfile new file mode 100644 index 0000000000000000000000000000000000000000..e8c70cd6d3b74514e3bb27476e4642ceac74894e --- /dev/null +++ b/mkfile @@ -0,0 +1,19 @@ +all:V:init decoder/joeblack.wasm encoder/joeblack + cp decoder/joeblack.wasm . + +init:V: + cp shared/shared.go decoder/ + cp shared/wordlist decoder/ + cp shared/shared.go encoder/ + cp shared/wordlist encoder/ + +decoder/joeblack.wasm: decoder/joeblack.go + cd decoder/ && mk + +encoder/joeblack: encoder/joeblack.go + cd encoder/ && mk + +clean:V: + rm -f decoder/joeblack.wasm decoder/shared.go decoder/wordlist + rm -f encoder/joeblack encoder/shared.go encoder/wordlist + rm -f encoder/secrets.dirty diff --git a/shared/shared.go b/shared/shared.go new file mode 100644 index 0000000000000000000000000000000000000000..430b7ff9565223eda45a5d77ae67811dd965657c --- /dev/null +++ b/shared/shared.go @@ -0,0 +1,101 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "fmt" + "io" + "math/big" + "strings" +) + +type SecretEntry struct { + Hidden bool + Value string +} +type Secret map[string]SecretEntry + +type Payload struct { + Keys [][]byte + Secrets []byte +} + +func encrypt(plaintext, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, fmt.Errorf("while creating cipher: %w\n", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("while creating gcm: %w\n", err) + } + + nonce := make([]byte, gcm.NonceSize()) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return nil, fmt.Errorf("while creating nonce: %w\n", err) + } + + return gcm.Seal(nonce, nonce, plaintext, nil), nil +} + +func decrypt(ciphertext, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + if len(ciphertext) < gcm.NonceSize() { + return nil, errors.New("malformed ciphertext") + } + + return gcm.Open(nil, + ciphertext[:gcm.NonceSize()], + ciphertext[gcm.NonceSize():], + nil, + ) +} + +func toWords(bytes []byte, wordlist []string) []string { + words := []string{} + var z, r big.Int + base := big.NewInt(2048) + zero := big.NewInt(0) + z.SetBytes(bytes) // note big-endian + + j := 0 + for z.Cmp(zero) > 0 { + z.QuoRem(&z, base, &r) + words = append(words, wordlist[r.Int64()]) + j++ + } + return words +} + +func fromWords(share string, wordlist map[string]int) ([]byte, error) { + var word, exp, pos, z big.Int + base := big.NewInt(2048) + share = strings.ReplaceAll(share, ",", "") + words := strings.Fields(share) + for j := 0; j < len(words); j++ { + w := words[j] + index, present := wordlist[w] + if !present { + return nil, errors.New("no word " + w) + } + word.SetInt64(int64(index)) + exp.SetInt64(int64(j)) + pos.Exp(base, &exp, nil) + word.Mul(&word, &pos) + z.Add(&z, &word) + } + return z.Bytes(), nil +} diff --git a/shared/wordlist b/shared/wordlist new file mode 100644 index 0000000000000000000000000000000000000000..5eb606d34969f25e0f7d8b7e517f64765adfbb9a --- /dev/null +++ b/shared/wordlist @@ -0,0 +1,2048 @@ +abolicja +abonent +absurd +adiunkt +adopcja +adorator +adres +adwokat +aerozol +agat +agitator +agnostyk +agonia +agrafka +agrest +ajent +akacja +akademia +akapit +akcyza +akord +aktor +akustyka +akwarium +akwedukt +alarm +albatros +album +alchemik +alegoria +alergia +aleja +alfabet +algebra +algorytm +alibi +alkohol +almanach +alpaka +altruizm +amator +ambasada +ambicja +ambona +ambulans +ameba +ametyst +amfibia +amnestia +amulet +amunicja +ananas +anakonda +analiza +anarchia +anatomia +anegdota +aneks +anemik +animacja +ankieta +anoda +anomalia +antena +antracyt +antylopa +aorta +aparat +apaszka +apelacja +apetyt +aplauz +aplikant +apogeum +apostrof +aprobata +apteka +arbiter +archiwum +arena +areszt +argument +arkusz +armia +arogant +aromat +arszenik +artefakt +artysta +asceta +aster +astma +asystent +atleta +atrakcja +audycja +aukcja +aureola +autentyk +awans +awers +awizo +awokado +babcia +baca +badanie +badyl +bagatela +bagnet +bajka +bakcyl +bakteria +balans +balkon +balsam +bania +baobab +baran +barok +bariera +barszcz +barwnik +barykada +basen +basista +baszta +batat +bazar +beczka +bednarz +befsztyk +begonia +bejca +bejsbol +beksa +belka +benzyna +beszamel +beton +bibelot +biceps +biedak +biegun +biesiada +bigos +bijatyka +bikini +bilard +bilet +bingo +biodro +biologia +biszkopt +bitwa +biuro +biwak +biznes +bizon +blacha +blankiet +blask +blef +blender +blizna +blok +blondyn +bluszcz +bluza +boazeria +bobas +bobslej +bochenek +bocian +boczek +bodziec +bogactwo +bohater +boisko +bojkot +boks +bolec +bomba +bonanza +bonus +borowik +borsuk +bosak +bosman +botoks +botwinka +brama +brat +brawura +brelok +brezent +broda +brzask +brzuch +brzytwa +bubel +buda +budowa +budynek +budzik +bufet +bufor +buhaj +bukiet +bukszpan +buldog +bulgot +bulwa +bumerang +bunkier +bunt +burak +burza +busola +busz +butelka +butik +buziak +bystrzak +bywalec +bzdura +cacko +cebula +cecha +cedr +cedzak +celownik +celuloza +cement +cennik +centrum +cenzura +cera +cesarz +cewka +chaber +chaos +charyzma +chaszcze +chata +chemia +chichot +chirurg +chleb +chlor +chluba +chmiel +chmura +chochla +chodnik +choinka +chojrak +cholewa +chomik +choroba +chrapka +chrom +chrust +chrypa +chrzan +chuchro +chusta +chwast +chwila +chwyt +ciarki +ciastko +ciecz +cielak +ciemnia +cietrzew +cios +ciocia +cisza +ciuchcia +ciupaga +cmentarz +cudak +cugle +cukinia +cuma +cyborg +cyfra +cygaro +cyjanek +cykl +cykoria +cykuta +cynamon +cyngiel +cynizm +cypel +cyrk +cyrograf +cytat +cytryna +cywil +czajka +czapla +czar +czaszka +czek +czepek +czerwiec +czkawka +czosnek +czujnik +czupryna +czytnik +dach +daktyl +dalmierz +dandys +data +dawka +debata +debet +decybel +dedukcja +deficyt +deflacja +dekada +dekiel +dekolt +delegat +delfin +delicja +delta +demobil +dentysta +depesza +depozyt +depresja +deptak +deputat +desant +deser +deska +despota +deszcz +detektyw +detoks +dewiza +dezerter +diadem +diagram +dialog +diament +dieta +diler +dioda +dioptria +dogmat +doktryna +dokument +dolar +dolewka +dolina +domena +domino +donacja +donica +donos +dopamina +dopust +doradca +dorobek +dorzecze +dostatek +dotyk +dowcip +doza +doznanie +dozorca +drabina +dramat +drapacz +draska +drewno +drezyna +drganie +drink +droga +drozd +drut +drwal +drwina +dryblas +dryg +drzazga +drzewo +drzwi +dubler +dudek +duet +dukat +durszlak +dusza +dworzec +dygresja +dyktando +dylemat +dymisja +dynamit +dyplom +dyrektor +dyrygent +dysk +dywan +dywersja +dywizja +dzban +dziennik +dzicz +dzikus +dziupla +dzwon +dzyndzel +echo +eden +edukacja +edytor +efekt +egoista +egzamin +egzotyka +ekierka +ekipa +ekologia +ekonomia +ekran +ekspert +ekstrakt +elaborat +eldorado +elegant +elektryk +element +elewacja +eliksir +elita +embargo +emblemat +emeryt +emocje +emulsja +enklawa +enzym +epidemia +epitet +epoka +epopeja +eremita +erozja +errata +erudyta +erupcja +esej +esencja +eskapada +eskimos +eskorta +estakada +estrada +estymata +etat +eter +etos +etyka +eufemizm +euforia +ewolucja +fabryka +facet +fajrant +fakir +fakultet +faktura +fala +falbana +falset +fanatyk +fantazja +faraon +farba +farmer +fart +fasada +fasola +fatum +fatyga +faul +fauna +faworyt +felga +ferajna +ferwor +festyn +feta +fiasko +figiel +figura +fikcja +filia +film +filozof +filtr +finanse +finezja +finisz +fiolka +fiord +firana +firma +fiszka +fizjolog +fizyka +flakon +flaming +flanela +flara +flaszka +flet +flinta +flirt +flisak +floret +fluid +fluor +fobia +foch +foka +folder +folklor +folwark +fonetyka +fontanna +fortuna +forum +forsa +fosfor +fotel +fotograf +frajda +frakcja +framuga +frazes +fretka +frez +freon +front +fryzura +fuga +fuksja +fundacja +funkcja +furia +furora +furtka +fusy +fuszerka +futro +fuzja +gablota +gacek +gajowy +galareta +galeria +galop +gambit +gang +garb +garnek +garstka +gatunek +gawron +gazda +gazela +gbur +gejzer +gekon +geniusz +geodeta +geolog +gepard +gerbera +gest +giermek +gigant +gildia +gilotyna +gips +girlanda +gitara +gleba +glina +glob +glonojad +gloria +gluten +gmach +gnat +gniazdo +gniew +godzina +gofr +gogle +gokart +golarka +golf +golonka +gondola +goniec +gorczyca +gorset +goryl +gospoda +grabie +gracja +grafit +grahamka +gram +grasica +gratis +grejfrut +grobla +groch +gromada +grosz +groteska +groza +gruchot +grunt +grupa +gruszka +gruz +gryf +gryka +grymas +gryps +grysik +grzanka +grzbiet +grzejnik +grzmot +grzyb +gulasz +guziec +gwardia +gwiazda +gwizdek +gzyms +habit +haft +halibut +halogen +hamak +hamownia +hamulec +handel +hangar +haracz +harcerz +harem +harfa +harmonia +haubica +haust +hazard +heban +heca +herb +herszt +hiacynt +hibiskus +hipnoza +hipoteka +historia +hobby +hokej +holownik +homar +honor +horda +horror +hossa +hostel +hrabia +hubka +hufiec +humor +huragan +hycel +hymn +idea +idiom +idol +idylla +ignorant +igraszka +igrzyska +ikona +iloraz +iluzja +imbir +imbus +imiennik +imitacja +impas +impet +implant +import +impreza +impuls +incydent +indukcja +indyk +inercja +infekcja +inkasent +insomnia +instynkt +insulina +interes +inwazja +inwestor +irokez +ironia +irys +irytacja +iskra +istota +iteracja +izba +izolacja +jacht +jadalnia +jagoda +jaguar +jajko +jamnik +jantar +jarmark +jarosz +jarzyna +jaskinia +jawor +jedwab +jedzenie +jelito +jelonek +jesiotr +jezdnia +jezioro +jodyna +jogurt +jojo +joker +jota +jubiler +judo +juhas +junior +jupiter +juror +jutrznia +kabaret +kaczor +kadr +kafar +kaganiec +kajuta +kaktus +kalarepa +kamfora +kangur +kant +kaptur +karmnik +karp +karuzela +kaskada +kasownik +kasta +kasyno +kasztan +katorga +kaucja +kawa +kawior +kciuk +kefir +keks +kelner +kemping +kicz +kierat +kikut +kilometr +kimono +kino +kita +kitel +kiwi +klawisz +klej +kleszcz +klips +klon +klomb +klub +kminek +knajpa +knedel +knieja +knot +koala +kobra +kocur +kofeina +kogut +koja +kojot +kokarda +kolczyk +komin +komora +komputer +konkurs +kopyto +kormoran +kornik +kosmita +koszyk +kotwica +kowboj +kozak +kozetka +kpina +krab +krater +krasnal +kreacja +kreda +krewetka +kret +krezus +krochmal +krokodyl +kromka +kronika +kropla +krosno +kruk +kruszec +krwinka +krykiet +krzesiwo +ksero +ksywa +kubek +kubatura +kucharz +kufajka +kufer +kuglarz +kujon +kulig +kumpel +kuna +kunszt +kupiec +kurort +kustosz +kuweta +kuzyn +kwadrat +kwas +kwatera +kwestia +kwiat +kwota +ladaco +laguna +laik +lambada +lament +laminat +lampart +lanie +lapsus +laptop +larum +larwa +laser +laska +lasso +latarka +lateks +lato +laufer +laurka +lawenda +lawina +lebioda +lecytyna +lecznica +legenda +legwan +lejek +lekarz +lekcja +leksykon +lektor +leming +lenistwo +lewarek +lewica +liana +licencja +lico +liczba +lider +liga +lignina +likier +limit +limonka +limuzyna +linia +linoleum +lipiec +list +litania +litera +litr +lizak +lodowiec +logarytm +logika +logo +lokata +lombard +lont +lornetka +loteria +lotnisko +lubczyk +ludojad +lufa +lukr +luksus +lumpeks +lunatyk +luneta +lustro +machina +macocha +maczuga +magazyn +magister +magma +magnolia +majestat +majonez +majster +makaron +makler +makowiec +makrela +maksyma +malarz +malina +malunek +malwa +mamba +mamut +mandat +manewr +mangusta +manifest +mankiet +manowce +manto +mapa +maraton +marchew +marka +marmur +marsz +maruda +marynarz +marzec +maskotka +masyw +maszt +materac +matryca +matura +mazak +mazurek +mebel +mecenas +mech +medalion +mediana +megafon +meldunek +melisa +melodia +membrana +mentor +menzurka +metal +metoda +metr +mewa +miasto +miazga +miednica +mielizna +mienie +mierzeja +migawka +migracja +mikrob +mikser +milion +mimika +mimoza +mina +mintaj +minuta +miot +miriada +mirra +miruna +misiek +misja +mistrz +mitoman +mizeria +mlecz +mnemonik +mnich +mocarz +moczary +model +modlitwa +moher +molo +moment +monarcha +moneta +monitor +monter +monument +morale +morena +mors +morwa +morze +most +motor +mozaika +mrowisko +mrzonka +mszyca +mucha +mulat +mulda +mumia +mundur +murawa +mustang +muszla +mutacja +muza +muzeum +muzyka +mysz +myto +nacisk +naczepa +nadajnik +nadkole +nadruk +nadwozie +nadzieja +nafta +nagana +nagietek +nagonka +nagrobek +najemnik +nakaz +naklejka +nakrycie +nalewka +nalot +namiot +napar +napis +narada +narcyz +narkoza +narracja +narybek +narzuta +naszywka +natrysk +nauka +nawiew +nawojka +nawrot +nawyk +nazwisko +negatyw +nektar +nenufar +nepotyzm +nerw +neuron +niebo +niewola +niezdara +nimfa +nioska +nitka +nizina +nobel +nocleg +noga +nokaut +norka +notacja +notes +nowela +nozdrza +nubuk +nudziarz +numer +nurek +nurt +nuta +nutria +nylon +oaza +obawa +obcas +obejma +obelga +oberek +obiad +obiekt +oblicze +obora +obraz +obrona +obsesja +obszar +obuch +obuwie +ocean +ocet +ochrona +oczy +oddech +odkrycie +odlew +odlot +odludzie +odmowa +odpad +odpust +odsetek +odsiecz +odsysacz +odwaga +odwyk +odyseja +odznaka +odzysk +ofensywa +oferta +ofiara +oficer +ogar +ogier +ogniwo +ogon +ogrodnik +ogryzek +ohyda +ojciec +okaz +okleina +okolica +okop +okruch +okrzyk +oktawa +okulista +olbrzym +olcha +oleander +oliwka +omam +omega +omlet +omnibus +oparcie +opcja +operacja +opieka +opinia +opis +opresja +optyk +opuncja +orbita +orchidea +orczyk +order +oregano +organ +orient +origami +orszak +orzech +osada +oscypek +osesek +osiedle +osika +osoba +osocze +ostoja +ostryga +owacja +owad +owca +owczarek +owies +owoc +ozdoba +pacjent +pacynka +padlina +pagon +pajac +pajda +palestra +paliwo +palma +palnik +paluch +panaceum +panel +panierka +panorama +pantera +papaja +papirus +papla +papryka +papuga +para +parcela +parkiet +parodia +partia +parytet +pasek +pasmo +pasterz +pasztet +patent +patriota +pauza +pawian +pazur +pedagog +pediatra +peleryna +pelikan +pensja +perz +pestka +petarda +petunia +petycja +pianista +pieczywo +piekarz +pieprz +piernik +pies +pigwa +pijawka +pika +pikieta +pikle +piknik +piksel +pilates +pilot +pinezka +pingwin +pion +piosenka +pipeta +piramida +piruet +pisak +pisklak +pistolet +piszczyk +pitbull +piwnica +piwonia +pizza +placebo +plagiat +plajta +plakat +plama +planeta +plaster +platyna +plebania +plecak +plejada +plener +plik +pliszka +plomba +plotka +plucha +pluskwa +pniak +pobocze +pocisk +poczta +podatek +poddasze +podest +podium +podkop +podmuch +podpis +poduszka +podwozie +podziw +poemat +poezja +pogarda +pogoda +pohybel +pojazd +pojemnik +pokora +pokrzywa +pokuta +polana +polemika +polityk +pomelo +pomidor +pomnik +pomoc +pompa +pomruk +pomyje +ponton +popyt +porada +porcja +porost +port +posag +postawa +posucha +poszlaka +potas +potop +potrawa +potwarz +potylica +powaga +powiew +pozew +poziom +pozycja +praktyk +pranie +prasa +prawda +precel +predator +prefiks +premia +procent +produkt +profesor +program +projekt +promocja +prorok +prosiak +prymus +pryzmat +przodek +przysmak +psikus +pszenica +ptak +publika +puchacz +pucybut +pucz +puder +puenta +pukiel +pula +pulpit +pumeks +punkt +pupil +puszcza +puzon +puzzle +pycha +pytajnik +pyton +pyza +rabat +rabunek +rachunek +racica +racuch +radar +radca +radio +radny +radosny +rafa +rajd +rakieta +ranczo +randka +ranga +rapier +raport +ratownik +ratrak +rdest +rdza +reakcja +rebelia +rebus +recepta +reforma +refren +regaty +region +rejon +rejs +rekin +reklama +rekord +rekrut +relaks +religia +remiza +remont +renegat +renifer +replika +resor +respekt +reszka +retoryk +retusz +reumatyk +rewers +rewir +rezerwa +rezonans +ring +riposta +robot +rodak +rodzic +rogal +rolada +roleta +rolnik +romans +rondo +ropa +ropucha +rosiczka +rosomak +roszada +rotunda +rower +rozalia +rozkaz +rozmowa +rozpacz +rozrywka +rozum +rozwaga +rubryka +rudera +ruina +rukola +rulon +rumak +rumianek +runda +runo +rura +ruszt +rutyna +rwetes +ryba +rybitwa +rycerz +rycina +rydel +rydwan +rydz +rygor +rykoszet +rynek +rynna +rysopis +rysunek +rytm +rywal +ryzyko +rzemyk +rzepa +rzodkiew +rzut +sadyba +sadza +sakwa +saletra +salto +samolot +samuraj +sandacz +saper +sardynka +sarkazm +sarna +sasanka +satelita +satyra +scena +sceptyk +schab +schemat +schody +schron +seans +sedno +sejf +sekator +sekret +sekunda +sekwoja +seler +semafor +senat +senior +sensacja +serce +serdelek +serenada +seria +serweta +sesja +sezam +sezon +sfera +siad +siano +siarka +siedziba +siekiera +sielanka +sierota +sikorka +silnik +silos +siniak +siostra +sito +siwizna +sjesta +skakanka +skalpel +skandal +skarb +skaut +skaza +skiba +sklep +sknera +skobel +skoczek +skorupa +skos +skrobia +skrucha +skrypt +skrzyp +skup +skuter +skwer +slalom +slang +smalec +smardz +smog +smutek +snajper +snob +socha +soczewka +sojusz +sombrero +sonar +sonda +sopel +sopran +sorbet +sosna +sowa +spacer +spadek +spaniel +spawacz +spazm +spec +spedycja +spektrum +spiekota +spinacz +spirala +splot +spodnie +spoiwo +sprawa +spryt +srebro +sroka +ssak +stacja +stado +stajnia +start +statek +staw +stempel +step +sternik +stocznia +stojak +stok +stolica +stonoga +stopa +strajk +strefa +stroik +struna +strych +strzelba +studia +stukot +styl +suchar +sufit +sufler +sugestia +sukces +suknia +sukurs +sumienie +surowiec +suszarka +suwak +sweter +swoboda +syfon +sygnet +sylaba +sylwetka +symfonia +sympatia +synteza +syrena +syrop +system +sytuacja +szabla +szacunek +szafir +szakal +szalik +szampon +szansa +szatyn +szczyt +szef +szejk +szelest +szept +szereg +szewc +szkic +szklanka +szkoda +szlaban +szlem +szmer +szminka +sznur +szofer +szpak +szpinak +szprycha +szpula +szrama +szron +sztab +sztuka +sztylet +szufla +szum +szutr +szwagier +szyba +szyfr +szyk +szyld +szympans +szynka +szyszka +tabaka +tabela +tablet +tabor +tabun +tafla +tajga +takt +talent +talia +talk +talon +tama +tamburyn +taniec +tango +tantiema +tapczan +tapeta +taras +tarcza +targ +tarnina +tartak +taryfa +tasak +tasiemka +tatarak +tawerna +tchawica +teatr +teflon +tekst +tektura +telefon +temat +temblak +tempo +tenis +tenor +teren +terier +termos +tilapia +tiul +tkanka +tlen +toast +tokarz +toksyna +tona +toner +tonik +topaz +topola +torba +torf +tornado +torpeda +tortura +totem +towar +tradycja +traf +tragarz +traktor +tramwaj +tranzyt +traper +tratwa +trauma +trawnik +trefl +trema +trend +trep +treser +triceps +triumf +trofeum +tron +troska +trotyl +trucht +trud +truizm +tryb +trzcina +trzepot +trzmiel +trzustka +tuba +tubylec +tukan +tuleja +tulipan +tundra +tunel +tupet +turniej +turysta +tuzin +twarz +twierdza +tworzywo +tygiel +tygrys +tykwa +tymianek +tynk +tyran +ubaw +ubranie +ucieczka +uczony +uczta +uczucie +udar +ufoludek +ujma +ukleja +ukrop +ulewa +ulga +ulica +umowa +umywalka +uncja +uniform +unik +upominek +uraza +urlop +usta +usterka +utarczka +utopia +uwaga +uzda +wachlarz +wakacje +walec +walka +walor +waluta +wampir +wandal +wanilia +wanna +wapno +wariat +warkocz +warta +warunek +warzywo +wazon +weksel +welon +welur +welwet +wentyl +weranda +werbel +wersja +werwa +wesele +wiadukt +wianek +wiara +wiatrak +wichura +widelec +widmo +widok +widz +wierzba +wieszak +wigor +wigwam +wiking +wiklina +wilczur +wilk +willa +winda +wiosna +wisiorek +wiskoza +witamina +witryna +wiwat +wiza +wizyta +wniosek +wnuk +woda +wojewoda +wolfram +wosk +wrak +wrona +wrzawa +wrzos +wstyd +wtorek +wujek +wulkan +wuwuzela +wycisk +wyczyn +wydra +wygoda +wykres +wymiar +wynik +wypiek +wyprawa +wyraz +wyspa +wystawa +wytrych +wywar +wzorzec +wzrok +zabawa +zabieg +zabobon +zabytek +zadyszka +zagadka +zakalec +zakon +zakup +zakwas +zaleta +zamek +zamsz +zapach +zapora +zaprawa +zarys +zasadzka +zasilacz +zaspa +zasuwa +zatoka +zawias +zawodnik +zbawca +zbir +zbiornik +zbroja +zbyt +zdanie +zderzak +zdobywca +zdrapka +zdrowie +zebra +zegar +zemsta +zenit +zero +zestaw +zgoda +zgrzyt +ziarno +zielarz +ziemia +zima +zjawa +zlew +zlot +zmiana +zmora +zmywacz +znak +znalazca +znicz +zodiak +zorza +zraszacz +zraz +zszywka +zuch +zupa +zwarcie +zwida +zwierz +zygzak \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000000000000000000000000000000000000..80626eff673aaa39a00485a7be9f0dfc8020630e --- /dev/null +++ b/style.css @@ -0,0 +1,196 @@ +:root { + --accent: #745016; + --accent-hover: #000000; + --toa: #ffffff; + --text: #292929; + --bg: #fafafa; + --border: #737373; + --link: #005b85; + --link-on-dark: #1ab6ff; + --visited: #680bea; + --terminal-bg: #292929; + --terminal-text: #cdb360; + --dark-bg: #212121; + --text-on-dark: #fafafa; + --yellow: #db9d3b; + --black: #292929; +} + +@media (prefers-color-scheme: dark) { + :root { + --accent: #db9d3b; + --accent-hover: #ffffff; + --toa: #000000; + --text: #fafafa; + --bg: #292929; + --border: #919191; + --link: #38c0ff; + --link-on-dark: #1ab6ff; + --visited: #c7a4f9; + --terminal-bg: #292929; + --terminal-text: #cdb360; + --dark-bg: #212121; + --text-on-dark: #fafafa; + --yellow: #db9d3b; + --black: #292929; + } +} + +* { + box-sizing: border-box; + color: var(--text); + background: var(--bg); +} + +a { + color: var(--link); + text-decoration-color: var(--link); +} + +a:visited { + color: var(--visited); + text-decoration-color: var(--visited); +} + +a:hover, a:focus, a:visited:focus, a:visited:hover { + color: var(--accent); + text-decoration-color: var(--accent); +} + +p { + max-width: 80ch; + font-family: "IBM Plex Serif", serif; +} + +pre { + font-family: Iosevka, monospace; + background: var(--terminal-bg); + color: var(--terminal-text); + overflow: scroll; + white-space: pre; + max-width: 80ch; + border: 2px solid var(--border); + display: block; + padding: 1rem; + margin-bottom: 1rem; +} + +code { + font-family: Iosevka, monospace; + background: var(--terminal-bg); + color: var(--terminal-text); + overflow: scroll; + white-space: pre; +} + +section { + margin-bottom: 2rem; + width: 85%; + background: var(--dark-bg); + color: var(--text-on-dark); + padding-left: 1rem; + padding-right: 1rem; + padding-bottom: .5rem; +} + +section > h3 { + background: var(--dark-bg); + color: var(--link-on-dark); + padding: .5rem 0; + display: block; + border-bottom: 1px solid #e9ecef; + display: flex; + flex-wrap: wrap; +} + +section > p { + margin-top: .5rem; + font-size: 1rem; + background: var(--dark-bg); + color: var(--text-on-dark); +} + +button, input[type=submit] { + background: var(--accent); + color: var(--toa); + border: var(--text) 1px solid; + cursor: pointer; +} + +button:hover, button:active, button:focus, +input[type=submit]:hover, input[type=submit]:active, input[type=submit]:focus { + background: var(--accent-hover); +} + +button > a, button > a:hover, button > a:focus, button > a:visited:focus, button > a:visited:hover{ + background: inherit; + color: var(--toa); + text-decoration: none; +} + +h1, h2, h3, h4, h5, h6 { + font-family: "Fira Sans", sans-serif; +} + +body { + margin: 1rem; + display: flex; + flex-wrap: wrap; +} + +header { + font-weight: bold; + width: 100%; + font-size: 3rem; + font-family: "Fira Sans", sans-serif; +} + +nav { + width: 100%; + margin-bottom: 2rem; + font-family: Iosevka, monospace; +} + +nav > ul { + margin: .5rem 0; + padding: 0; + list-style-type: none; +} + +nav > ul > li { + display: inline; +} + +main { + max-width: 92%; + flex-grow: 11; + margin: 0 1rem 2rem 1rem; +} + +input, textarea { + border: var(--border) 1px solid; + display: block; + margin: .25rem 0 .75rem 0; +} + +label { + display: block; + font-family: "Fira Sans", sans-serif; +} + +aside { + width: 200px; + flex-grow: 1; +} + +aside > a { + display: block; + font-family: Iosevka, monospace; + margin-top: .1rem; + margin-bottom: 1rem; +} + +.warning * { + background: var(--yellow); + color: var(--black); +} diff --git a/wasm_exec.js b/wasm_exec.js new file mode 100644 index 0000000000000000000000000000000000000000..9ce6a20c3ffa82debd6853a72f02c316ccfb642d --- /dev/null +++ b/wasm_exec.js @@ -0,0 +1,554 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +"use strict"; + +(() => { + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!globalThis.fs) { + let outputBuf = ""; + globalThis.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substr(0, nl)); + outputBuf = outputBuf.substr(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { callback(enosys()); }, + chown(path, uid, gid, callback) { callback(enosys()); }, + close(fd, callback) { callback(enosys()); }, + fchmod(fd, mode, callback) { callback(enosys()); }, + fchown(fd, uid, gid, callback) { callback(enosys()); }, + fstat(fd, callback) { callback(enosys()); }, + fsync(fd, callback) { callback(null); }, + ftruncate(fd, length, callback) { callback(enosys()); }, + lchown(path, uid, gid, callback) { callback(enosys()); }, + link(path, link, callback) { callback(enosys()); }, + lstat(path, callback) { callback(enosys()); }, + mkdir(path, perm, callback) { callback(enosys()); }, + open(path, flags, mode, callback) { callback(enosys()); }, + read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, + readdir(path, callback) { callback(enosys()); }, + readlink(path, callback) { callback(enosys()); }, + rename(from, to, callback) { callback(enosys()); }, + rmdir(path, callback) { callback(enosys()); }, + stat(path, callback) { callback(enosys()); }, + symlink(path, link, callback) { callback(enosys()); }, + truncate(path, length, callback) { callback(enosys()); }, + unlink(path, callback) { callback(enosys()); }, + utimes(path, atime, mtime, callback) { callback(enosys()); }, + }; + } + + if (!globalThis.process) { + globalThis.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!globalThis.crypto) { + throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); + } + + if (!globalThis.performance) { + throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); + } + + if (!globalThis.TextEncoder) { + throw new Error("globalThis.TextEncoder is not available, polyfill required"); + } + + if (!globalThis.TextDecoder) { + throw new Error("globalThis.TextDecoder is not available, polyfill required"); + } + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + + globalThis.Go = class { + constructor() { + this.argv = ["js"]; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = resolve; + }); + this._pendingEvent = null; + this._scheduledTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const setInt64 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); + } + + const getInt64 = (addr) => { + const low = this.mem.getUint32(addr + 0, true); + const high = this.mem.getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = this.mem.getFloat64(addr, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = this.mem.getUint32(addr, true); + return this._values[id]; + } + + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; + + if (typeof v === "number" && v !== 0) { + if (isNaN(v)) { + this.mem.setUint32(addr + 4, nanHead, true); + this.mem.setUint32(addr, 0, true); + return; + } + this.mem.setFloat64(addr, v, true); + return; + } + + if (v === undefined) { + this.mem.setFloat64(addr, 0, true); + return; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = this._values.length; + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 0; + switch (typeof v) { + case "object": + if (v !== null) { + typeFlag = 1; + } + break; + case "string": + typeFlag = 2; + break; + case "symbol": + typeFlag = 3; + break; + case "function": + typeFlag = 4; + break; + } + this.mem.setUint32(addr + 4, nanHead | typeFlag, true); + this.mem.setUint32(addr, id, true); + } + + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); + } + + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + go: { + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). + // This changes the SP, thus we have to update the SP used by the imported function. + + // func wasmExit(code int32) + "runtime.wasmExit": (sp) => { + sp >>>= 0; + const code = this.mem.getInt32(sp + 8, true); + this.exited = true; + delete this._inst; + delete this._values; + delete this._goRefCounts; + delete this._ids; + delete this._idPool; + this.exit(code); + }, + + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + "runtime.wasmWrite": (sp) => { + sp >>>= 0; + const fd = getInt64(sp + 8); + const p = getInt64(sp + 16); + const n = this.mem.getInt32(sp + 24, true); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); + }, + + // func resetMemoryDataView() + "runtime.resetMemoryDataView": (sp) => { + sp >>>= 0; + this.mem = new DataView(this._inst.exports.mem.buffer); + }, + + // func nanotime1() int64 + "runtime.nanotime1": (sp) => { + sp >>>= 0; + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); + }, + + // func walltime() (sec int64, nsec int32) + "runtime.walltime": (sp) => { + sp >>>= 0; + const msec = (new Date).getTime(); + setInt64(sp + 8, msec / 1000); + this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); + }, + + // func scheduleTimeoutEvent(delay int64) int32 + "runtime.scheduleTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._scheduledTimeouts.set(id, setTimeout( + () => { + this._resume(); + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn("scheduleTimeoutEvent: missed timeout event"); + this._resume(); + } + }, + getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early + )); + this.mem.setInt32(sp + 16, id, true); + }, + + // func clearTimeoutEvent(id int32) + "runtime.clearTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this.mem.getInt32(sp + 8, true); + clearTimeout(this._scheduledTimeouts.get(id)); + this._scheduledTimeouts.delete(id); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + sp >>>= 0; + crypto.getRandomValues(loadSlice(sp + 8)); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (sp) => { + sp >>>= 0; + const id = this.mem.getUint32(sp + 8, true); + this._goRefCounts[id]--; + if (this._goRefCounts[id] === 0) { + const v = this._values[id]; + this._values[id] = null; + this._ids.delete(v); + this._idPool.push(id); + } + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (sp) => { + sp >>>= 0; + storeValue(sp + 24, loadString(sp + 8)); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (sp) => { + sp >>>= 0; + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 32, result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (sp) => { + sp >>>= 0; + Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (sp) => { + sp >>>= 0; + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const m = Reflect.get(v, loadString(sp + 16)); + const args = loadSliceOfValues(sp + 32); + const result = Reflect.apply(m, v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, result); + this.mem.setUint8(sp + 64, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, err); + this.mem.setUint8(sp + 64, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.apply(v, undefined, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.construct(v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (sp) => { + sp >>>= 0; + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (sp) => { + sp >>>= 0; + const str = encoder.encode(String(loadValue(sp + 8))); + storeValue(sp + 16, str); + setInt64(sp + 24, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (sp) => { + sp >>>= 0; + const str = loadValue(sp + 8); + loadSlice(sp + 16).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (sp) => { + sp >>>= 0; + this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (sp) => { + sp >>>= 0; + const dst = loadSlice(sp + 8); + const src = loadValue(sp + 32); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + // func copyBytesToJS(dst ref, src []byte) (int, bool) + "syscall/js.copyBytesToJS": (sp) => { + sp >>>= 0; + const dst = loadValue(sp + 8); + const src = loadSlice(sp + 16); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + "debug": (value) => { + console.log(value); + }, + } + }; + } + + async run(instance) { + if (!(instance instanceof WebAssembly.Instance)) { + throw new Error("Go.run: WebAssembly.Instance expected"); + } + this._inst = instance; + this.mem = new DataView(this._inst.exports.mem.buffer); + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + globalThis, + this, + ]; + this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map([ // mapping from JS values to reference ids + [0, 1], + [null, 2], + [true, 3], + [false, 4], + [globalThis, 5], + [this, 6], + ]); + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096; + + const strPtr = (str) => { + const ptr = offset; + const bytes = encoder.encode(str + "\0"); + new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); + offset += bytes.length; + if (offset % 8 !== 0) { + offset += 8 - (offset % 8); + } + return ptr; + }; + + const argc = this.argv.length; + + const argvPtrs = []; + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)); + }); + argvPtrs.push(0); + + const keys = Object.keys(this.env).sort(); + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); + }); + argvPtrs.push(0); + + const argv = offset; + argvPtrs.forEach((ptr) => { + this.mem.setUint32(offset, ptr, true); + this.mem.setUint32(offset + 4, 0, true); + offset += 8; + }); + + // The linker guarantees global data starts from at least wasmMinDataAddr. + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. + const wasmMinDataAddr = 4096 + 8192; + if (offset >= wasmMinDataAddr) { + throw new Error("total length of command line and environment variables exceeds limit"); + } + + this._inst.exports.run(argc, argv); + if (this.exited) { + this._resolveExitPromise(); + } + await this._exitPromise; + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } +})();