joeblack.git

commit d6365dff54ba9aebcd04fc8ff2bc64199efa8b16

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;
+			};
+		}
+	}
+})();