├── .dockerignore ├── .editorconfig ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── onion-icon.png └── shrek-session.webp ├── cmd └── shrek │ ├── appoptions.go │ ├── log.go │ └── main.go ├── examples ├── README.md ├── coalminer │ └── main.go ├── go.mod ├── go.sum ├── helloworld │ ├── landing-page.html │ └── main.go └── ogrequotes │ ├── main.go │ ├── quotes.go │ └── quotes.json ├── go.mod ├── go.sum ├── internal └── ed25519 │ ├── ed25519.go │ ├── ed25519_test.go │ ├── iterator.go │ ├── iterator_test.go │ └── scalaradd.go ├── matcher.go ├── matcher_test.go ├── miner.go ├── onionaddress.go └── onionaddress_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.dockerignore 2 | **/.env 3 | **/.git 4 | **/.gitattributes 5 | **/.gitignore 6 | **/.idea 7 | **/.vscode 8 | **/bin 9 | **/build 10 | **/deploy 11 | **/docker-compose* 12 | **/Dockerfile* 13 | **/releases 14 | assets 15 | examples 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | indent_style = space 8 | indent_size = 2 9 | 10 | [{Makefile,go.mod,go.sum,*.go,.gitmodules}] 11 | indent_style = tab 12 | indent_size = 4 13 | 14 | [*.md] 15 | indent_size = 4 16 | trim_trailing_whitespace = false 17 | 18 | [Dockerfile] 19 | indent_size = 4 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE directories 2 | .idea/ 3 | .vscode/ 4 | 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Outputs of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | test-coverage.html 18 | 19 | # Dependency directories 20 | vendor/ 21 | 22 | # Release archives 23 | *.tar.gz 24 | *.zip 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-alpine AS builder 2 | 3 | WORKDIR /usr/src/shrek 4 | 5 | # Pre-copy/cache go.mod for pre-downloading dependencies and only re-downloading 6 | # them in subsequent builds if they change. 7 | COPY go.mod go.sum ./ 8 | RUN go mod download && go mod verify 9 | 10 | # Copy all other project files. 11 | COPY . . 12 | 13 | # Build app. 14 | RUN go build -v -o /usr/local/bin/shrek ./cmd/shrek 15 | 16 | FROM alpine:latest AS final 17 | 18 | WORKDIR /app 19 | 20 | # Copy compiled binary into final image. 21 | COPY --from=builder /usr/local/bin/shrek . 22 | 23 | # Define entry point. 24 | ENTRYPOINT ["/app/shrek", "-d", "/app/generated/"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 innix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Shrek :onion: 4 | 5 | [![Go Reference ][goref-badge]][goref-page]  6 | [![GitHub Release ][ghrel-badge]][ghrel-page]  7 | [![Project License][licen-badge]][licen-page]  8 | 9 | 10 | 11 | Shrek is a vanity `.onion` address generator. 12 | 13 | Runs on Linux, macOS, Windows, and more. A single static binary with no dependencies. 14 | 15 | ![Shrek running from CLI](./assets/shrek-session.webp) 16 | 17 |
18 | 19 | # Install 20 | 21 | You can download a pre-compiled build from the [GitHub Releases][ghrel-page] page. 22 | 23 | Or you can build it from source: 24 | 25 | ```bash 26 | go install github.com/innix/shrek/cmd/shrek@latest 27 | ``` 28 | 29 | This will place the compiled Shrek binary in your `$GOPATH/bin` directory. 30 | 31 | # Getting started 32 | 33 | Shrek is a single binary that is normally used via the CLI. The program takes 1 or 34 | more filters as arguments. 35 | 36 | ```bash 37 | # Generate an address that starts with "food": 38 | shrek food 39 | 40 | # Generate an address that starts with "food" and ends with "xid": 41 | shrek food:xid 42 | 43 | # Generate an address that starts with "food" and ends with "xid", or starts with "barn": 44 | shrek food:xid barn 45 | 46 | # Generate an address that ends with "2ayd". 47 | shrek :2ayd 48 | 49 | # Shrek can search for the start of an onion address much faster than the end of the 50 | # address. Therefore, it is recommended that the filters you use have a bigger start 51 | # filter and a smaller (or zero) end filter. 52 | ``` 53 | 54 | To see full usage, use the help flag `-h`: 55 | 56 | ```bash 57 | shrek -h 58 | ``` 59 | 60 | # Running in Docker 61 | 62 | [![Docker Hub][dkhub-badge]][dkhub-page] 63 | 64 | You can run Shrek in Docker by pulling a pre-built image from [Docker Hub][dkhub-page] 65 | or by building an image locally. 66 | 67 | ## Option 1: Pull image from Docker Hub 68 | 69 | There's no need to run `docker pull` directly, because `docker run` will automatically 70 | download the image from Docker Hub if it isn't found locally. So you can save a step 71 | and run Shrek with a one-liner: 72 | 73 | ```bash 74 | docker run --rm -it \ 75 | -v "$PWD/generated":/app/generated \ 76 | innix/shrek:latest \ 77 | -n 3 food:ad barn:yd 78 | ``` 79 | 80 | ## Option 2: Build image locally 81 | 82 | Build the Docker image locally using the [`Dockerfile`](Dockerfile): 83 | 84 | ```bash 85 | docker build -t shrek:latest . 86 | ``` 87 | 88 | Then run a new container using the locally built image: 89 | 90 | ```bash 91 | docker run --rm -it \ 92 | -v "$PWD/generated":/app/generated \ 93 | shrek:latest \ 94 | -n 3 food:ad barn:yd 95 | ``` 96 | 97 | ## How does the `docker run` command work with Shrek? 98 | 99 | When running Shrek in a container, you pass arguments to it by placing them at the 100 | very end of the `docker run` command. In the example commands above, the arguments 101 | `-n 3 food:ad barn:yd` are passed to the Shrek binary. 102 | 103 | The Docker image configures Shrek to save found `.onion` addresses to the directory 104 | `/app/generated/` in the container's file system. To access that directory from the 105 | host, you need to create a shared volume using Docker's `-v` argument. In the above 106 | examples, the `-v` argument will allow the host to access the container's directory 107 | by browsing to `$PWD/generated`. 108 | 109 | If you're getting a permission error when trying to access the `generated` directory 110 | on the host, take a look at [this FAQ][docker-access-dir-faq]. 111 | 112 | # Using Shrek as a library 113 | 114 | You can use Shrek as a library in your Go code. Add it to your `go.mod` file by running 115 | this in your project root: 116 | 117 | ```bash 118 | go get github.com/innix/shrek 119 | ``` 120 | 121 | Here's an example of using Shrek to find an address using a start-end pattern and 122 | saving it to disk: 123 | 124 | ```go 125 | package main 126 | 127 | import ( 128 | "context" 129 | "fmt" 130 | 131 | "github.com/innix/shrek" 132 | ) 133 | 134 | func main() { 135 | // Brute-force find a hostname that starts with "foo" and ends with "id". 136 | addr, err := shrek.MineOnionHostName(context.Background(), nil, shrek.StartEndMatcher{ 137 | Start: []byte("foo"), 138 | End: []byte("id"), 139 | }) 140 | if err != nil { 141 | panic(err) 142 | } 143 | 144 | // Save hostname and the public/secret keys to disk. 145 | fmt.Printf("Onion address %q found, saving to file system.\n", addr.HostNameString()) 146 | err = shrek.SaveOnionAddress("output_dir", addr) 147 | if err != nil { 148 | panic(err) 149 | } 150 | } 151 | ``` 152 | 153 | More comprehensive examples of how to use Shrek as a library can be found in the 154 | [examples](./examples) directory. 155 | 156 | # In active development 157 | 158 | This project is under active development and hasn't reached `v1.0` yet. Therefore the public 159 | API is not fully stable and may contain breaking changes as new versions are released. If 160 | you're using Shrek as a package in your code and an update breaks it, feel free to open an 161 | issue and a contributor will help you out. 162 | 163 | Once Shrek reaches `v1.0`, the API will stabilize and any new changes will not break your 164 | existing code. 165 | 166 | # Performance and goals 167 | 168 | Shrek is the fastest `.onion` vanity address generator written in Go (at time of writing), but 169 | it's still slow compared to the mature and highly optimized [`mkp224o`][mkp224o-page] program. 170 | There are optimizations that could be made to Shrek to improve its performance. Contributions 171 | are welcome and encouraged. Feel free to open an issue/discussion to share your thoughts and 172 | ideas, or submit a pull request with your optimization. 173 | 174 | The primary goal of Shrek is to be an easy to use CLI program for regular users and 175 | library for Go developers, not to be the fastest program out there. It should be able 176 | to run on every major platform that Go supports. Use of `cgo` or other complicated 177 | build processes should be avoided. 178 | 179 | # FAQ 180 | 181 | ## Are v2 addresses supported? 182 | 183 | No. They were already deprecated at the time Shrek was first released, so there didn't 184 | seem any point supporting them. There are no plans to add them as a feature. 185 | 186 | ## Can I run Shrek without all the emojis, extra colors, and other fancy formatting? 187 | 188 | Sure. Use the `--format basic` flag when running the program to keep things simple. 189 | You can also run `shrek --help` to see a list of all possible formatting options; 190 | maybe you'll find one you like. 191 | 192 | ## How do I use a generated address with the [`cretz/bine`][ghbine-page] Tor library? 193 | 194 | There are [example projects](./examples) that show how to use Shrek and Bine together. 195 | 196 | But to put it simply, all you need to do is convert Shrek's `OnionAddress` into Bine's 197 | `KeyPair`. See this code example: 198 | 199 |
200 | Click to expand code snippet. 201 | 202 | ```go 203 | package main 204 | 205 | import ( 206 | "github.com/cretz/bine/tor" 207 | "github.com/cretz/bine/torutil/ed25519" 208 | "github.com/innix/shrek" 209 | ) 210 | 211 | func main() { 212 | // Generate any address, the value doesn't matter for this demo. 213 | addr, err := shrek.GenerateOnionAddress(nil) 214 | if err != nil { 215 | panic(err) 216 | } 217 | 218 | // Or read a previously generated address that was saved to disk with SaveOnionAddress. 219 | // addr, err := shrek.ReadOnionAddress("./addrs/bqyql3bq532kzihcmp3c6lb6id.onion/") 220 | // if err != nil { 221 | // panic(err) 222 | // } 223 | 224 | // Take the private key from Shrek's OnionAddress and turn it into an ed25519.KeyPair 225 | // that the Bine library can understand. 226 | keyPair := ed25519.PrivateKey(addr.SecretKey).KeyPair() 227 | 228 | // Now you can use the KeyPair in Bine as you normally would, e.g. with ListenConf: 229 | listenConf := &tor.ListenConf{ 230 | Key: keyPair, 231 | } 232 | } 233 | ``` 234 |
235 | 236 | ## Why can't I access the `generated` directory created by the Docker container? 237 | 238 | If the directory (or any of the sub-directories) were created by the Shrek process 239 | running in the Docker container, they will be owned by the `root` user that is running 240 | the Shrek process. You need to change ownership of the directory back to your user, 241 | which can be done by running the [`chown`][chown.1-page] command: 242 | 243 | ```bash 244 | sudo chown -R $USER "$PWD/generated" 245 | ``` 246 | 247 | ## Why "Shrek"? 248 | 249 | Onions have layers, ogres have layers. 250 | 251 | _This project has no affiliation with the creators of the Shrek franchise. It's just a 252 | pop culture reference joke._ 253 | 254 | # License 255 | 256 | Shrek is distributed under the terms of the MIT License (see [LICENSE](LICENSE)). 257 | 258 | 259 | 260 | [goref-badge]: 261 | [goref-page]: "Go pkg.dev" 262 | 263 | [ghrel-badge]: 264 | [ghrel-page]: "GitHub Releases" 265 | 266 | [gorep-badge]: 267 | [gorep-page]: "Go Report" 268 | 269 | [gover-badge]: 270 | [gover-page]: "Go Version" 271 | 272 | [licen-badge]: 273 | [licen-page]: "Project License" 274 | 275 | [dkhub-badge]: 276 | [dkhub-page]: "innix/shrek - Docker Hub page" 277 | 278 | [mkp224o-page]: "cathugger/mkp224o - GitHub page" 279 | [ghbine-page]: "cretz/bine - GitHub page" 280 | [chown.1-page]: "chown(1) - Linux man page" 281 | 282 | [docker-access-dir-faq]: <#why-cant-i-access-the-generated-directory-created-by-the-docker-container> "Why can't I access the generated directory created by the Docker container?" 283 | -------------------------------------------------------------------------------- /assets/onion-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/innix/shrek/ae64fb0fddd98b8c1ff43abedc16b5cae1d9de27/assets/onion-icon.png -------------------------------------------------------------------------------- /assets/shrek-session.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/innix/shrek/ae64fb0fddd98b8c1ff43abedc16b5cae1d9de27/assets/shrek-session.webp -------------------------------------------------------------------------------- /cmd/shrek/appoptions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type appOptions struct { 9 | NumAddresses int 10 | SaveDirectory string 11 | NumThreads int 12 | Formatting formatting 13 | Patterns []string 14 | } 15 | 16 | type formatting string 17 | 18 | const ( 19 | BasicFormatting = formatting("basic") 20 | ColorFormatting = formatting("colored") 21 | EnhancedFormatting = formatting("enhanced") 22 | AllFormatting = formatting("") 23 | ) 24 | 25 | func (f *formatting) String() string { 26 | return string(*f) 27 | } 28 | 29 | func (f *formatting) Set(v string) error { 30 | fv := formatting(strings.ToLower(v)) 31 | 32 | switch fv { 33 | case BasicFormatting, ColorFormatting, EnhancedFormatting, AllFormatting: 34 | *f = fv 35 | return nil 36 | case "all": 37 | *f = AllFormatting 38 | return nil 39 | default: 40 | return fmt.Errorf("parsing %q: invalid format kind", v) 41 | } 42 | } 43 | 44 | func (f *formatting) Type() string { 45 | return "string" 46 | } 47 | 48 | func (f *formatting) UseColors() bool { 49 | return *f == ColorFormatting || *f == AllFormatting 50 | } 51 | 52 | func (f *formatting) UseEnhanced() bool { 53 | return *f == EnhancedFormatting || *f == AllFormatting 54 | } 55 | -------------------------------------------------------------------------------- /cmd/shrek/log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | LogVerboseEnabled = false 11 | LogPrettyEnabled = false 12 | ) 13 | 14 | func LogError(format string, a ...interface{}) { 15 | // Remove "shrek: " from error messages before outputting them. 16 | for i, v := range a { 17 | if err, ok := v.(error); ok && err != nil { 18 | a[i] = strings.ReplaceAll(err.Error(), "shrek: ", "") 19 | } 20 | } 21 | 22 | _, _ = fmt.Fprintln(os.Stderr, fmt.Sprintf(format, a...)) 23 | } 24 | 25 | func LogInfo(format string, a ...interface{}) { 26 | _, _ = fmt.Fprintln(os.Stdout, fmt.Sprintf(format, a...)) 27 | } 28 | 29 | func LogVerbose(format string, a ...interface{}) { 30 | if LogVerboseEnabled { 31 | _, _ = fmt.Fprintln(os.Stdout, fmt.Sprintf(format, a...)) 32 | } 33 | } 34 | 35 | func Pretty(text, alt string) string { 36 | if LogPrettyEnabled { 37 | return text 38 | } 39 | return alt 40 | } 41 | -------------------------------------------------------------------------------- /cmd/shrek/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/briandowns/spinner" 16 | "github.com/fatih/color" 17 | "github.com/innix/shrek" 18 | "github.com/spf13/pflag" 19 | ) 20 | 21 | const ( 22 | appName = "shrek" 23 | appVersion = "0.6.1" 24 | ) 25 | 26 | func main() { 27 | opts := buildAppOptions() 28 | runtime.GOMAXPROCS(opts.NumThreads + 1) // +1 for main proc. 29 | 30 | LogVerboseEnabled = true 31 | LogPrettyEnabled = opts.Formatting.UseEnhanced() 32 | color.NoColor = !opts.Formatting.UseColors() 33 | 34 | LogInfo("%sSaving found addresses to %s", 35 | Pretty("📁 ", ""), 36 | color.YellowString("%s", opts.SaveDirectory), 37 | ) 38 | LogInfo("") 39 | 40 | m, err := buildMatcher(opts.Patterns) 41 | if err != nil { 42 | LogError("%s: Could not build search filters: %v.", color.RedString("Error"), err) 43 | os.Exit(2) 44 | } 45 | 46 | addrText := color.GreenString("%d", opts.NumAddresses) 47 | if opts.NumAddresses == 0 { 48 | addrText = color.GreenString("infinite") 49 | } 50 | LogInfo("%sSearching for %s addresses, using %s threads, with %s search filters:", 51 | Pretty("🔥 ", ""), 52 | addrText, 53 | color.GreenString("%d", opts.NumThreads), 54 | color.GreenString("%d", len(m.Inner)), 55 | ) 56 | defer func() { 57 | LogInfo("") 58 | LogInfo("%sShrek has finished searching.", Pretty("👍 ", "")) 59 | }() 60 | 61 | // Channel to receive onion addresses from miners. 62 | addrs := make(chan *shrek.OnionAddress, opts.NumAddresses) 63 | 64 | ctx, cancel := context.WithCancel(context.Background()) 65 | defer cancel() 66 | 67 | // Spin up the miners. 68 | wg := runWorkGroup(opts.NumThreads, func(n int) { 69 | if err := mineHostNames(ctx, addrs, m); err != nil && !errors.Is(err, ctx.Err()) { 70 | LogError("%s: %v.", color.RedString("Error"), err) 71 | } 72 | }) 73 | 74 | // Loop until the requested number of addresses have been mined. 75 | mineForever := opts.NumAddresses == 0 76 | ps := newProgressSpinner(" ", time.Millisecond*130) 77 | for i := 0; i < opts.NumAddresses || mineForever; i++ { 78 | ps.Start() 79 | addr := <-addrs 80 | hostname := addr.HostNameString() 81 | ps.Stop() 82 | 83 | LogInfo("%s%s", Pretty(" 🔹 ", ""), hostname) 84 | if err := shrek.SaveOnionAddress(opts.SaveDirectory, addr); err != nil { 85 | LogError("%s: Found .onion but could not save it to file system: %v.", 86 | color.RedString("Error"), 87 | err, 88 | ) 89 | } 90 | } 91 | 92 | cancel() 93 | wg.Wait() 94 | } 95 | 96 | func buildAppOptions() appOptions { 97 | var opts appOptions 98 | 99 | pflag.IntVarP(&opts.NumAddresses, "onions", "n", 0, "`num`ber of onion addresses to generate, 0 = infinite (default = 1)") 100 | pflag.StringVarP(&opts.SaveDirectory, "save-dir", "d", "", "`dir`ectory to save addresses in (default = cwd)") 101 | pflag.IntVarP(&opts.NumThreads, "threads", "t", 0, "`num`ber of threads to use (default = all CPU cores)") 102 | pflag.VarP(&opts.Formatting, "format", "", "what `kind` of formatting to use (basic, colored, enhanced, default = all)") 103 | 104 | var help, version bool 105 | pflag.BoolVarP(&help, "help", "h", false, "show this help menu") 106 | pflag.BoolVarP(&version, "version", "v", false, "show app version") 107 | 108 | pflag.CommandLine.SortFlags = false 109 | pflag.Usage = func() { 110 | LogError("Usage:") 111 | LogError(" %s [options] filter [more-filters...]", filepath.Base(os.Args[0])) 112 | LogError("") 113 | LogError("OPTIONS") 114 | pflag.PrintDefaults() 115 | } 116 | pflag.Parse() 117 | 118 | // Set non-zero defaults here to prevent pflag from printing the default values itself. 119 | // It can't be disabled and we want to print it differently from how pflag does it. 120 | if f := pflag.Lookup("onions"); !f.Changed { 121 | if err := f.Value.Set("1"); err != nil { 122 | panic(err) 123 | } 124 | } 125 | 126 | if version { 127 | LogInfo("%s %s, os: %s, arch: %s", appName, appVersion, runtime.GOOS, runtime.GOARCH) 128 | os.Exit(0) 129 | } else if help { 130 | pflag.Usage() 131 | os.Exit(0) 132 | } else if pflag.NArg() < 1 { 133 | LogError("No filters provided.") 134 | LogError("") 135 | pflag.Usage() 136 | os.Exit(2) 137 | } 138 | 139 | // Set runtime to use number of threads requested. 140 | if opts.NumThreads <= 0 { 141 | opts.NumThreads = runtime.NumCPU() 142 | } 143 | 144 | // Set to default if negative number given for some reason. 145 | if opts.NumAddresses < 0 { 146 | opts.NumAddresses = 0 147 | } 148 | 149 | // Translate save dir to absolute dir. Serves no logical purpose, just improves logging. 150 | if !filepath.IsAbs(opts.SaveDirectory) { 151 | absd, err := filepath.Abs(opts.SaveDirectory) 152 | if err != nil { 153 | LogError("%s: Could not resolve save dir to absolute path: %v.", 154 | color.RedString("Error"), 155 | err, 156 | ) 157 | os.Exit(1) 158 | } 159 | opts.SaveDirectory = absd 160 | } 161 | 162 | // Non-flag args are patterns. 163 | opts.Patterns = pflag.Args() 164 | 165 | return opts 166 | } 167 | 168 | func buildMatcher(args []string) (shrek.MultiMatcher, error) { 169 | var mm shrek.MultiMatcher 170 | var inner []shrek.StartEndMatcher 171 | 172 | for _, pattern := range args { 173 | parts := strings.Split(pattern, ":") 174 | 175 | switch len(parts) { 176 | case 1: 177 | start := parts[0] 178 | m := shrek.StartEndMatcher{ 179 | Start: []byte(start), 180 | End: nil, 181 | } 182 | if err := m.Validate(); err != nil { 183 | return mm, fmt.Errorf( 184 | "pattern '%s' is not valid: %w", color.YellowString("%s", pattern), err, 185 | ) 186 | } 187 | inner = append(inner, m) 188 | case 2: 189 | start, end := parts[0], parts[1] 190 | m := shrek.StartEndMatcher{ 191 | Start: []byte(start), 192 | End: []byte(end), 193 | } 194 | if err := m.Validate(); err != nil { 195 | return mm, fmt.Errorf( 196 | "pattern '%s' is not valid: %w", color.YellowString("%s", pattern), err, 197 | ) 198 | } 199 | inner = append(inner, m) 200 | default: 201 | return mm, fmt.Errorf( 202 | "pattern '%s' is not a valid syntax", color.YellowString("%s", pattern), 203 | ) 204 | } 205 | } 206 | 207 | LogVerbose("%sLooking for addresses that match any of these conditions:", Pretty("🔎 ", "")) 208 | for _, m := range inner { 209 | startsWith := fmt.Sprintf("'%s'", color.YellowString("%s", m.Start)) 210 | endsWith := fmt.Sprintf("'%s'", color.YellowString("%s", m.End)) 211 | if len(m.Start) == 0 { 212 | startsWith = color.YellowString("anything") 213 | } 214 | if len(m.End) == 0 { 215 | endsWith = color.YellowString("anything") 216 | } 217 | 218 | LogVerbose("%sAn address that starts with %s and ends with %s", 219 | Pretty(" 🔸 ", " - "), 220 | startsWith, 221 | endsWith, 222 | ) 223 | 224 | mm.Inner = append(mm.Inner, m) 225 | } 226 | LogVerbose("") 227 | 228 | return mm, nil 229 | } 230 | 231 | func runWorkGroup(n int, fn func(n int)) *sync.WaitGroup { 232 | var wg sync.WaitGroup 233 | wg.Add(n) 234 | 235 | for i := 0; i < n; i++ { 236 | go func(i int) { 237 | defer wg.Done() 238 | fn(i) 239 | }(i) 240 | } 241 | 242 | return &wg 243 | } 244 | 245 | func mineHostNames(ctx context.Context, ch chan<- *shrek.OnionAddress, m shrek.Matcher) error { 246 | for ctx.Err() == nil { 247 | addr, err := shrek.MineOnionHostName(ctx, nil, m) 248 | if err != nil { 249 | return err 250 | } 251 | 252 | select { 253 | case ch <- addr: 254 | case <-ctx.Done(): 255 | } 256 | } 257 | 258 | return ctx.Err() 259 | } 260 | 261 | func newProgressSpinner(prefix string, speed time.Duration) *spinner.Spinner { 262 | s := spinner.New(spinner.CharSets[14], speed) 263 | s.HideCursor = true 264 | s.Prefix = prefix 265 | s.Suffix = " " 266 | s.Writer = os.Stderr 267 | 268 | if !LogPrettyEnabled { 269 | s.Delay = time.Second * 30 270 | s.HideCursor = false 271 | s.Writer = io.Discard 272 | } 273 | 274 | return s 275 | } 276 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Shrek code examples 2 | 3 | This directory contains a few example projects that show how to use Shrek as a 4 | library in your Go project. 5 | 6 | # Coal Miner 7 | 8 | The Coal Miner project is a very basic vanity `.onion` address finder. It searches 9 | for 5 `.onion` addresses that start with the word "coal". It outputs each address 10 | to the console once it finds it, then exits once all 5 have been found. It only uses 11 | a single goroutine, which means there's no concurrency; it only searches for one 12 | address at a time. This was done intentionally to keep the code simple. 13 | 14 | To start the program, open a console in this directory and run: 15 | 16 | ```bash 17 | go run ./coalminer/ 18 | ``` 19 | 20 | # Hello World on Tor 21 | 22 | 🧅 _You'll need Tor installed on your computer to run this example._ 23 | 24 | The Hello World project is a very basic HTTP server that runs on the Tor network. It 25 | hosts a [single static HTML file](./helloworld/landing-page.html), which contains 26 | some very basic information about the Shrek project. 27 | 28 | When the program is started, it will use Shrek to generate an `.onion` address. You 29 | can specify what the address should start with by passing a string to the program as 30 | the first argument. If you don't provide one, then "ogre" will be used by default. 31 | The address might take a minute or two to generate, depending on how powerful your 32 | computer is. 33 | 34 | After an address has been found, the program will use it to configure an HTTP server 35 | to run on the Tor network. Once connected to the Tor network, the program will output 36 | a full `.onion` URL to the console. Copy and paste it into a Tor Browser, and you 37 | should see the `landing-page.html` page load. That link will work for everyone who 38 | has a Tor Browser. 39 | 40 | To start the program, open a console in this directory and run: 41 | 42 | ```bash 43 | # Search for .onion address that starts with default value "ogre". 44 | go run ./helloworld/ 45 | 46 | # Search for .onion address that starts with custom value "fizz". 47 | go run ./helloworld/ fizz 48 | ``` 49 | 50 | # Random Ogre Quotes on Tor 51 | 52 | 🧅 _You'll need Tor installed on your computer to run this example._ 53 | 54 | The Random Ogre Quotes project is another basic HTTP server that runs on the Tor 55 | network. It hosts a single HTML page which contains a random quote from the Shrek 56 | movie. The quote changes everytime you refresh the page. 57 | 58 | When the program is started, it will use Shrek to generate an `.onion` address that 59 | starts with "ogre". The address might take a minute or two to generate, depending 60 | on how powerful your computer is. 61 | 62 | After an address has been found, the program will use it to configure an HTTP server 63 | to run on the Tor network. Once connected to the Tor network, the program will output 64 | a full `.onion` URL to the console. Copy and paste it into a Tor Browser, and you 65 | should see the page load with a random Shrek quote. That link will work for everyone 66 | who has a Tor Browser. 67 | 68 | To start the program, open a console in this directory and run: 69 | 70 | ```bash 71 | go run ./ogrequotes/ 72 | ``` 73 | 74 | # Acknowledgements 75 | 76 | Thanks to the [`cretz/bine`](https://github.com/cretz/bine/) project for making using 77 | Tor with Go a breeze. Some of the the examples use it to connect to Tor. 78 | -------------------------------------------------------------------------------- /examples/coalminer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/innix/shrek" 9 | ) 10 | 11 | func main() { 12 | const addrsToFind = 5 13 | 14 | fmt.Println("This app will find", addrsToFind, ".onion addrs that start with 'coal'.") 15 | fmt.Println("Time to mine some coal... (press Ctrl+C to stop the program)") 16 | fmt.Println() 17 | time.Sleep(time.Second) 18 | 19 | coalMatcher := shrek.StartEndMatcher{ 20 | Start: []byte("coal"), 21 | } 22 | 23 | for i := 0; i < addrsToFind; i++ { 24 | fmt.Print("Mining coal lump #", i+1, "... ") 25 | 26 | addr, err := shrek.MineOnionHostName(context.Background(), nil, coalMatcher) 27 | if err != nil { 28 | fmt.Println("mining failed, moving to next one...") 29 | fmt.Println("The error was:", err) 30 | 31 | continue 32 | } 33 | 34 | fmt.Println("got it:", addr.HostNameString()) 35 | } 36 | 37 | fmt.Println() 38 | fmt.Println("All coal lumps have been mined!") 39 | } 40 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/innix/shrek/examples 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/cretz/bine v0.2.0 7 | github.com/innix/shrek v0.6.0 8 | ) 9 | 10 | require ( 11 | github.com/oasisprotocol/curve25519-voi v0.0.0-20210908142542-2a44edfcaeb0 // indirect 12 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect 13 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect 14 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/briandowns/spinner v1.18.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= 2 | github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= 3 | github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= 4 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 7 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 8 | github.com/innix/shrek v0.6.0 h1:qN5YPbkQiDed6/7k3rneUL6ElTriYAn+G/CHOUmcAtQ= 9 | github.com/innix/shrek v0.6.0/go.mod h1:5+KnPnLSbBxb6DtgGbUu0Xv6afVfSYIUkndCjz2xw6M= 10 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 11 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 12 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 13 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 14 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 15 | github.com/oasisprotocol/curve25519-voi v0.0.0-20210908142542-2a44edfcaeb0 h1:mhi9nviuY1liEwHRApVyaY/5B7JQMU2/vpm/qHk7pdc= 16 | github.com/oasisprotocol/curve25519-voi v0.0.0-20210908142542-2a44edfcaeb0/go.mod h1:WUcXjUd98qaCVFb6j8Xc87MsKeMCXDu9Nk8JRJ9SeC8= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 22 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 24 | golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 25 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= 26 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 27 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 28 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= 29 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 30 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 31 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 37 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 39 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 40 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 41 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 42 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 43 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 44 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | -------------------------------------------------------------------------------- /examples/helloworld/landing-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Shrek Onion Generator - Hello World 9 | 10 | 48 | 49 | 50 | 51 |
52 |

Shrek

53 |

A vanity .onion address generator.

54 |

Hello world! You are running this web page on Tor!

55 | 56 |

57 | This is a very simple web page, and if you're reading this then you've 58 | browsed to it from a Tor browser. The .onion address being used by Tor 59 | was generated by Shrek, a vanity .onion address generator written in Go. 60 |

61 |

62 | Shrek is an open source project available on GitHub. It can be used as a 63 | CLI tool or as a library in your Go code. 64 |

65 | 66 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /examples/helloworld/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto" 7 | _ "embed" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/http" 12 | "os" 13 | "time" 14 | 15 | "github.com/cretz/bine/tor" 16 | "github.com/cretz/bine/torutil/ed25519" 17 | "github.com/innix/shrek" 18 | ) 19 | 20 | //go:embed landing-page.html 21 | var landingPageFileBytes []byte 22 | 23 | func main() { 24 | const defaultSearchTerm = "ogre" 25 | const addrSearchTimeout = time.Minute * 5 26 | const torStartupTimeout = time.Minute * 3 27 | 28 | searchTerm := defaultSearchTerm 29 | if len(os.Args) == 2 { 30 | searchTerm = os.Args[1] 31 | } 32 | 33 | ctx, cancel := context.WithTimeout(context.Background(), addrSearchTimeout) 34 | defer cancel() 35 | 36 | fmt.Printf("Searching for an .onion address starting with %q.\n", searchTerm) 37 | fmt.Println("This could take a minute or two.") 38 | if len(searchTerm) > 5 { 39 | fmt.Println("Warning: The search term is a bit long. It could take a while to find.") 40 | } 41 | 42 | matcher := shrek.StartEndMatcher{ 43 | Start: []byte(searchTerm), 44 | } 45 | 46 | addr, err := shrek.MineOnionHostName(ctx, nil, matcher) 47 | if err != nil && errors.Is(err, ctx.Err()) { 48 | fmt.Println("Your computer took too long trying to generate an address.") 49 | fmt.Println("Choose a shorter search term next time!") 50 | os.Exit(1) 51 | } else if err != nil { 52 | fmt.Println("Error: Could not generate an address:", err) 53 | os.Exit(2) 54 | } 55 | 56 | fmt.Println("Address found:", addr.HostNameString()) 57 | fmt.Println() 58 | fmt.Println("Starting Tor server using address. This could take a minute.") 59 | 60 | t, err := tor.Start(context.Background(), nil) 61 | if err != nil { 62 | fmt.Println("Error: Could not start Tor instance:", err) 63 | os.Exit(3) 64 | } 65 | defer t.Close() 66 | 67 | // Convert the Shrek OnionAddress into a KeyPair that Bine can work with. 68 | kp := ed25519.PrivateKey(addr.SecretKey).KeyPair() 69 | 70 | onion, err := createTorListener(torStartupTimeout, t, kp) 71 | if err != nil { 72 | fmt.Println("Error: Could not create Tor listener:", err) 73 | os.Exit(4) 74 | } 75 | defer onion.Close() 76 | 77 | fmt.Printf("Open a Tor-enabled browser and browse to: http://%s.onion/\n", onion.ID) 78 | fmt.Println() 79 | fmt.Println("Press Ctrl-C to close server.") 80 | fmt.Println() 81 | 82 | if err := serveLandingPage(onion); err != nil && !errors.Is(err, http.ErrServerClosed) { 83 | fmt.Println("Error: Could not serve HTTP requests over Tor:", err) 84 | os.Exit(5) 85 | } 86 | } 87 | 88 | func createTorListener(timeout time.Duration, t *tor.Tor, key crypto.PrivateKey) (*tor.OnionService, error) { 89 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 90 | defer cancel() 91 | 92 | onion, err := t.Listen(ctx, &tor.ListenConf{ 93 | LocalPort: 8080, 94 | RemotePorts: []int{80}, 95 | Key: key, 96 | }) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | return onion, nil 102 | } 103 | 104 | func serveLandingPage(l net.Listener) error { 105 | mux := http.NewServeMux() 106 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 107 | br := bytes.NewReader(landingPageFileBytes) 108 | http.ServeContent(w, r, "landing-page.html", time.Time{}, br) 109 | }) 110 | 111 | return http.Serve(l, mux) 112 | } 113 | -------------------------------------------------------------------------------- /examples/ogrequotes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto" 7 | "errors" 8 | "fmt" 9 | "math/rand" 10 | "net" 11 | "net/http" 12 | "os" 13 | "time" 14 | 15 | "github.com/cretz/bine/tor" 16 | "github.com/cretz/bine/torutil/ed25519" 17 | "github.com/innix/shrek" 18 | ) 19 | 20 | func main() { 21 | const searchTerm = "ogre" 22 | const addrSearchTimeout = time.Minute * 5 23 | const torStartupTimeout = time.Minute * 3 24 | 25 | ctx, cancel := context.WithTimeout(context.Background(), addrSearchTimeout) 26 | defer cancel() 27 | 28 | fmt.Printf("Searching for an .onion address starting with %q.\n", searchTerm) 29 | fmt.Println("This could take a minute or two.") 30 | 31 | matcher := shrek.StartEndMatcher{ 32 | Start: []byte(searchTerm), 33 | } 34 | 35 | addr, err := shrek.MineOnionHostName(ctx, nil, matcher) 36 | if err != nil && errors.Is(err, ctx.Err()) { 37 | fmt.Println("Your computer took too long trying to generate an address.") 38 | fmt.Println("Choose a shorter search term next time!") 39 | os.Exit(1) 40 | } else if err != nil { 41 | fmt.Println("Error: Could not generate the .onion address:", err) 42 | os.Exit(2) 43 | } 44 | 45 | fmt.Println("The .onion address found:", addr.HostNameString()) 46 | fmt.Println() 47 | fmt.Println("Starting Tor server using found address. This could take a minute.") 48 | 49 | t, err := tor.Start(context.Background(), nil) 50 | if err != nil { 51 | fmt.Println("Error: Could not start Tor instance:", err) 52 | os.Exit(3) 53 | } 54 | defer t.Close() 55 | 56 | // Convert the Shrek OnionAddress into a KeyPair that Bine can work with. 57 | kp := ed25519.PrivateKey(addr.SecretKey).KeyPair() 58 | 59 | onion, err := createTorListener(torStartupTimeout, t, kp) 60 | if err != nil { 61 | fmt.Println("Error: Could not create Tor listener:", err) 62 | os.Exit(4) 63 | } 64 | defer onion.Close() 65 | 66 | fmt.Printf("Open a Tor-enabled browser and browse to: http://%s.onion/\n", onion.ID) 67 | fmt.Println() 68 | fmt.Println("Press Ctrl-C to close server.") 69 | fmt.Println() 70 | 71 | if err := serveOgreQuotes(onion); err != nil && !errors.Is(err, http.ErrServerClosed) { 72 | fmt.Println("Error: Could not serve HTTP requests over Tor:", err) 73 | os.Exit(5) 74 | } 75 | } 76 | 77 | func createTorListener(timeout time.Duration, t *tor.Tor, key crypto.PrivateKey) (*tor.OnionService, error) { 78 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 79 | defer cancel() 80 | 81 | onion, err := t.Listen(ctx, &tor.ListenConf{ 82 | LocalPort: 8080, 83 | RemotePorts: []int{80}, 84 | Key: key, 85 | }) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return onion, nil 91 | } 92 | 93 | func serveOgreQuotes(l net.Listener) error { 94 | mux := http.NewServeMux() 95 | mux.HandleFunc("/", getQuoteHandler()) 96 | 97 | return http.Serve(l, mux) 98 | } 99 | 100 | func getQuoteHandler() http.HandlerFunc { 101 | ogreQuotes, err := loadOgreQuotes() 102 | if err != nil { 103 | panic(fmt.Errorf("could not load ogre quotes: %w", err)) 104 | } 105 | 106 | return func(w http.ResponseWriter, r *http.Request) { 107 | // Pick random quote. 108 | n := rand.Intn(len(ogreQuotes)) 109 | lines := ogreQuotes[n] 110 | 111 | // Create buffer and write HTML header to it. 112 | bb := &bytes.Buffer{} 113 | htmlHeader := fmt.Sprintf("

Ogre Quote #%d


\n\n", n+1) 114 | bb.WriteString(htmlHeader) 115 | 116 | // Write quote in HTML form to the buffer. 117 | for _, l := range lines { 118 | if len(l.Speaker) > 0 { 119 | bb.WriteString(fmt.Sprintf("%s: ", l.Speaker)) 120 | } 121 | bb.WriteString(fmt.Sprintf("%s
\n", l.Line)) 122 | } 123 | 124 | // Serve the buffer content to client. 125 | fmt.Printf("Serving ogre quote #%d to %s\n", n+1, r.UserAgent()) 126 | http.ServeContent(w, r, "index.html", time.Time{}, bytes.NewReader(bb.Bytes())) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /examples/ogrequotes/quotes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | type quoteLine struct { 10 | Speaker string `json:"speaker"` 11 | Line string `json:"line"` 12 | } 13 | 14 | type quote []quoteLine 15 | 16 | //go:embed quotes.json 17 | var ogreQuotesFile []byte 18 | 19 | func loadOgreQuotes() ([]quote, error) { 20 | var quotes []quote 21 | 22 | if err := json.Unmarshal(ogreQuotesFile, "es); err != nil { 23 | return nil, fmt.Errorf("could not unmarshal JSON quotes file: %w", err) 24 | } 25 | 26 | return quotes, nil 27 | } 28 | -------------------------------------------------------------------------------- /examples/ogrequotes/quotes.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | {"speaker": "Shrek", "line": "Here's a newsflash for you. Whether your parents like it or not... I am an ogre."} 4 | ], 5 | 6 | [ 7 | {"speaker": "Waiter", "line": "Bon Appétit!"}, 8 | {"speaker": "Donkey", "line": "Oooh, Mexican food! My favorite."} 9 | ], 10 | 11 | [ 12 | {"speaker": "Duloc singing toys", "line": "
"},
13 |         {"speaker": "", "line": "Welcome to Duloc, such a perfect town"},
14 |         {"speaker": "", "line": "Here we have some rules, let us lay them down"},
15 |         {"speaker": "", "line": "Don't make waves, stay in line"},
16 |         {"speaker": "", "line": "And we'll get along fine"},
17 |         {"speaker": "", "line": "Duloc is a perfect place"},
18 |         {"speaker": "", "line": "Keep your feet off the grass"},
19 |         {"speaker": "", "line": "Shine your shoes, wipe your... face"},
20 |         {"speaker": "", "line": "Duloc is, Duloc is"},
21 |         {"speaker": "", "line": "Duloc is a perfect place
"} 22 | ], 23 | 24 | [ 25 | {"speaker": "Shrek", "line": "Ogres... are like onions!"}, 26 | {"speaker": "Donkey", "line": "*sniff* They stink?"}, 27 | {"speaker": "Shrek", "line": "No!"}, 28 | {"speaker": "Donkey", "line": "Oh, they make you cry."}, 29 | {"speaker": "Shrek", "line": "Noo!"}, 30 | {"speaker": "Donkey", "line": "Ohhh, you leave them out in the Sun and they get all brown and start sprouting little white hairs."}, 31 | {"speaker": "Shrek", "line": "NOO!! Layers! Onions have layers, ogres have layers. You get it? We both have layers. *sighs heavily*"} 32 | ], 33 | 34 | [ 35 | {"speaker": "Donkey", "line": "Pheww! Who'd wana live in a place like that?"}, 36 | {"speaker": "Shrek", "line": "That... would be my home."}, 37 | {"speaker": "Donkey", "line": "Oh, and it is lovely! Just beautiful. You are quite a decorator. I like that boulder. That is a nice boulder."} 38 | ], 39 | 40 | [ 41 | {"speaker": "Shrek", "line": "Why are you following me?"}, 42 | {"speaker": "Donkey", "line": "*sings* 'Cause I'm all aloooone. There's no one heeere beside me. My problems have all gone. There's no one to derideeee me. But you gotta have faaaiii-"}, 43 | {"speaker": "Shrek", "line": "*interrupts, shouting* STOP SINGING! Well, it's no wonder you don't have any friends."}, 44 | {"speaker": "Donkey", "line": "Wow! Only a true friend would be that truly honest."} 45 | ], 46 | 47 | [ 48 | {"speaker": "Shrek", "line": "Listen, little donkey. Take a look at me. What am I?"}, 49 | {"speaker": "Donkey", "line": "Uhhh, really tall?"}, 50 | {"speaker": "Shrek", "line": "NO! I'm an ogre! You know, 'grab your torch and pitchforks'. Doesn't that bother you?"}, 51 | {"speaker": "Donkey", "line": "*shakes head with smile* Nope."}, 52 | {"speaker": "Shrek", "line": "Really?"}, 53 | {"speaker": "Donkey", "line": "Really, really."}, 54 | {"speaker": "Shrek", "line": "Oh."}, 55 | {"speaker": "Donkey", "line": "Man, I like you. What's your name?"}, 56 | {"speaker": "Shrek", "line": "...Shrek."} 57 | ] 58 | ] 59 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/innix/shrek 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/briandowns/spinner v1.18.1 7 | github.com/fatih/color v1.13.0 8 | github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae 9 | github.com/spf13/pflag v1.0.5 10 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 11 | ) 12 | 13 | require ( 14 | github.com/mattn/go-colorable v0.1.12 // indirect 15 | github.com/mattn/go-isatty v0.0.14 // indirect 16 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= 2 | github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= 3 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 4 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 5 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 6 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 7 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 8 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 9 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 10 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 11 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 12 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 13 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 14 | github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae h1:7smdlrfdcZic4VfsGKD2ulWL804a4GVphr4s7WZxGiY= 15 | github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= 16 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 17 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 18 | golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 19 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= 20 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 21 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 22 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 23 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 24 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 26 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/sys v0.0.0-20220325203850-36772127a21f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= 32 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 34 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 35 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 36 | -------------------------------------------------------------------------------- /internal/ed25519/ed25519.go: -------------------------------------------------------------------------------- 1 | package ed25519 2 | 3 | import ( 4 | "bytes" 5 | cryptorand "crypto/rand" 6 | "crypto/sha512" 7 | "errors" 8 | "fmt" 9 | "io" 10 | 11 | "github.com/oasisprotocol/curve25519-voi/curve" 12 | "github.com/oasisprotocol/curve25519-voi/curve/scalar" 13 | ) 14 | 15 | const ( 16 | // PublicKeySize is the size, in bytes, of public keys as used in this package. 17 | PublicKeySize = 32 18 | 19 | // PrivateKeySize is the size, in bytes, of private keys as used in this package. 20 | PrivateKeySize = 64 21 | 22 | // SeedSize is the size, in bytes, of private key seeds. 23 | SeedSize = 32 24 | ) 25 | 26 | // PrivateKey is the type of Ed25519 private keys. 27 | type PrivateKey []byte 28 | 29 | // PublicKey is the type of Ed25519 public keys. 30 | type PublicKey []byte 31 | 32 | // KeyPair is a type with both Ed25519 keys. 33 | type KeyPair struct { 34 | // PublicKey is the public key of the Ed25519 key pair. 35 | PublicKey PublicKey 36 | 37 | // PrivateKey is the private key of the Ed25519 key pair. 38 | PrivateKey PrivateKey 39 | } 40 | 41 | // Validate performs sanity checks to ensure that the public and private keys match. 42 | func (kp *KeyPair) Validate() error { 43 | pk, err := getPublicKeyFromPrivateKey(kp.PrivateKey) 44 | if err != nil { 45 | return fmt.Errorf("ed25519: could not compute public key from private key: %w", err) 46 | } 47 | 48 | if !bytes.Equal(kp.PublicKey, pk) { 49 | return errors.New("ed25519: keys do not match") 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func GenerateKey(rand io.Reader) (*KeyPair, error) { 56 | if rand == nil { 57 | rand = cryptorand.Reader 58 | } 59 | 60 | seed := make([]byte, SeedSize) 61 | if _, err := io.ReadFull(rand, seed); err != nil { 62 | return nil, fmt.Errorf("ed25519: could not read seed: %w", err) 63 | } 64 | 65 | sk := make([]byte, PrivateKeySize) 66 | newKeyFromSeed(sk, seed) 67 | 68 | // Private key does not contain the public key in this implementation, so we 69 | // need to compute it instead. 70 | pk, err := getPublicKeyFromPrivateKey(sk) 71 | if err != nil { 72 | return nil, fmt.Errorf("ed25519: could not compute public key from private key: %w", err) 73 | } 74 | 75 | return &KeyPair{ 76 | PublicKey: pk, 77 | PrivateKey: sk, 78 | }, nil 79 | } 80 | 81 | func newKeyFromSeed(sk, seed []byte) { 82 | if l := len(seed); l != SeedSize { 83 | panic(fmt.Sprintf("bad seed length: %d", l)) 84 | } 85 | 86 | digest := sha512.Sum512(seed) 87 | clampSecretKey(&digest) 88 | copy(sk, digest[:]) 89 | } 90 | 91 | func getPublicKeyFromPrivateKey(sk []byte) ([]byte, error) { 92 | if l := len(sk); l != PrivateKeySize { 93 | panic(fmt.Errorf("bad private key length: %d", len(sk))) 94 | } 95 | 96 | sc, err := scalar.NewFromBits(sk[:scalar.ScalarSize]) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | pk := curve.NewCompressedEdwardsY() 102 | pk.SetEdwardsPoint(curve.NewEdwardsPoint().MulBasepoint(curve.ED25519_BASEPOINT_TABLE, sc)) 103 | 104 | return pk[:], nil 105 | } 106 | 107 | func clampSecretKey(sk *[64]byte) { 108 | sk[0] &= 248 109 | sk[31] &= 63 110 | sk[31] |= 64 111 | } 112 | -------------------------------------------------------------------------------- /internal/ed25519/ed25519_test.go: -------------------------------------------------------------------------------- 1 | package ed25519_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/innix/shrek/internal/ed25519" 7 | ) 8 | 9 | func BenchmarkGenerateNewKey(b *testing.B) { 10 | for i := 0; i < b.N; i++ { 11 | _, err := ed25519.GenerateKey(nil) 12 | if err != nil { 13 | b.Fatalf("key pair generator errored unexpectedly during benchmark: %v", err) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/ed25519/iterator.go: -------------------------------------------------------------------------------- 1 | package ed25519 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "math" 8 | 9 | "github.com/oasisprotocol/curve25519-voi/curve" 10 | "github.com/oasisprotocol/curve25519-voi/curve/scalar" 11 | ) 12 | 13 | type keyIterator struct { 14 | kp *KeyPair 15 | eightPt *curve.EdwardsPoint 16 | 17 | pt *curve.EdwardsPoint 18 | sc *scalar.Scalar 19 | 20 | counter uint64 21 | } 22 | 23 | // NewKeyIterator creates and initializes a new Ed25519 key iterator. 24 | // The iterator is NOT thread safe; you must create a separate iterator for 25 | // each worker instead of sharing a single instance. 26 | func NewKeyIterator(rand io.Reader) (*keyIterator, error) { 27 | eightPt := curve.NewEdwardsPoint() 28 | eightPt = eightPt.MulBasepoint(curve.ED25519_BASEPOINT_TABLE, scalar.NewFromUint64(8)) 29 | 30 | it := &keyIterator{ 31 | eightPt: eightPt, 32 | } 33 | if _, err := it.init(rand); err != nil { 34 | return nil, err 35 | } 36 | 37 | return it, nil 38 | } 39 | 40 | func (it *keyIterator) Next() bool { 41 | const maxCounter = math.MaxUint64 - 8 42 | 43 | if it.counter > uint64(maxCounter) { 44 | return false 45 | } 46 | 47 | it.pt = it.pt.Add(it.pt, it.eightPt) 48 | it.counter += 8 49 | 50 | return true 51 | } 52 | 53 | func (it *keyIterator) PublicKey() PublicKey { 54 | var pk curve.CompressedEdwardsY 55 | pk.SetEdwardsPoint(it.pt) 56 | 57 | return pk[:] 58 | } 59 | 60 | func (it *keyIterator) PrivateKey() (PrivateKey, error) { 61 | sc := scalar.New().Set(it.sc) 62 | 63 | if it.counter > 0 { 64 | scalarAdd(sc, it.counter) 65 | } 66 | 67 | sk := make([]byte, PrivateKeySize) 68 | if err := sc.ToBytes(sk[:scalar.ScalarSize]); err != nil { 69 | panic(err) 70 | } 71 | copy(sk[scalar.ScalarSize:], it.kp.PrivateKey[scalar.ScalarSize:]) 72 | 73 | // Sanity check. 74 | if !((sk[0] & 248) == sk[0]) || !(((sk[31] & 63) | 64) == sk[31]) { 75 | return nil, errors.New("sanity check on private key failed") 76 | } 77 | 78 | return sk, nil 79 | } 80 | 81 | func (it *keyIterator) init(rand io.Reader) (*KeyPair, error) { 82 | kp, err := GenerateKey(rand) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | // Parse private key. 88 | sk, err := scalar.NewFromBits(kp.PrivateKey[:scalar.ScalarSize]) 89 | if err != nil { 90 | return nil, fmt.Errorf("ed25519: could not parse scalar from private key: %w", err) 91 | } 92 | 93 | // Parse public key. 94 | cpt, err := curve.NewCompressedEdwardsYFromBytes(kp.PublicKey) 95 | if err != nil { 96 | return nil, fmt.Errorf("ed25519: could not parse point from public key: %w", err) 97 | } 98 | pk := curve.NewEdwardsPoint() 99 | if _, err := pk.SetCompressedY(cpt); err != nil { 100 | return nil, fmt.Errorf("ed25519: could not decompress point from public key: %w", err) 101 | } 102 | 103 | // Cache data so it can be used later. 104 | it.kp = kp 105 | it.sc = sk 106 | it.pt = pk 107 | 108 | // Reset counter. 109 | it.counter = 0 110 | 111 | return kp, nil 112 | } 113 | -------------------------------------------------------------------------------- /internal/ed25519/iterator_test.go: -------------------------------------------------------------------------------- 1 | package ed25519_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/innix/shrek/internal/ed25519" 7 | ) 8 | 9 | func BenchmarkKeyIterator_PublicKeyAndNext(b *testing.B) { 10 | it, err := ed25519.NewKeyIterator(nil) 11 | if err != nil { 12 | b.Fatalf("could not create key iterator: %v", err) 13 | } 14 | 15 | b.ResetTimer() 16 | for i := 0; i < b.N; i++ { 17 | _ = it.PublicKey() 18 | if !it.Next() { 19 | b.Fatal("benchmark ran so fast it searched the entire address space, whew") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/ed25519/scalaradd.go: -------------------------------------------------------------------------------- 1 | package ed25519 2 | 3 | import ( 4 | "github.com/oasisprotocol/curve25519-voi/curve/scalar" 5 | ) 6 | 7 | func scalarAdd(dst *scalar.Scalar, v uint64) { 8 | var dstb [32]byte 9 | 10 | // Can't access scalar bytes publicly, so use ToBytes and SetBits. 11 | // Kinda slows things down, but have no other choice. 12 | 13 | if err := dst.ToBytes(dstb[:]); err != nil { 14 | panic(err) 15 | } 16 | 17 | scalarAddBytes(&dstb, v) 18 | 19 | if _, err := dst.SetBits(dstb[:]); err != nil { 20 | panic(err) 21 | } 22 | } 23 | 24 | func scalarAddBytes(dst *[32]byte, v uint64) { 25 | var carry uint32 26 | 27 | for i := 0; i < 32; i++ { 28 | carry += uint32(dst[i]) + uint32(v&0xFF) 29 | dst[i] = byte(carry & 0xFF) 30 | carry >>= 8 31 | 32 | v >>= 8 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /matcher.go: -------------------------------------------------------------------------------- 1 | package shrek 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type Matcher interface { 10 | MatchApprox(approx []byte) bool 11 | Match(exact []byte) bool 12 | } 13 | 14 | type StartEndMatcher struct { 15 | Start []byte 16 | End []byte 17 | } 18 | 19 | func (m StartEndMatcher) MatchApprox(approx []byte) bool { 20 | return bytes.HasPrefix(approx[:EncodedPublicKeyApproxSize], m.Start) 21 | } 22 | 23 | func (m StartEndMatcher) Match(exact []byte) bool { 24 | return bytes.HasPrefix(exact, m.Start) && bytes.HasSuffix(exact, m.End) 25 | } 26 | 27 | func (m StartEndMatcher) Validate() error { 28 | const validRunes = "abcdefghijklmnopqrstuvwxyz234567" 29 | const maxLength = 56 30 | 31 | // Check filter length isn't too long. 32 | if l := len(m.Start) + len(m.End); l > maxLength { 33 | return fmt.Errorf("shrek: filter is too long (%d > %d)", l, maxLength) 34 | } 35 | 36 | if len(m.Start) > 0 { 37 | // Check for invalid chars in Start. 38 | if invalid := strings.Trim(string(m.Start), validRunes); invalid != "" { 39 | return fmt.Errorf("shrek: start part contains invalid chars: %q", invalid) 40 | } 41 | } 42 | 43 | // If no end search filter, then there's nothing else to validate. 44 | // Return early to reduce indenting. 45 | if len(m.End) == 0 { 46 | return nil 47 | } 48 | 49 | // Check for invalid chars in End. 50 | if invalid := strings.Trim(string(m.End), validRunes); invalid != "" { 51 | return fmt.Errorf("shrek: end part contains invalid chars: %q", invalid) 52 | } 53 | 54 | // If last char isn't "d". 55 | if chr := string(m.End[len(m.End)-1]); chr != "d" { 56 | return fmt.Errorf("shrek: last char in end part must be %q, not %q", "d", chr) 57 | } 58 | 59 | if len(m.End) > 1 { 60 | // If 2nd last char isn't any of "aiqy". 61 | if chr := string(m.End[len(m.End)-2]); strings.Trim(chr, "aiqy") != "" { 62 | return fmt.Errorf("shrek: 2nd last char in end part must be one of %q, not %q", "aiqy", chr) 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | 69 | type MultiMatcher struct { 70 | Inner []Matcher 71 | 72 | // If All is true, then all the Inner matchers must match. If false, then only 1 of them 73 | // must match. 74 | All bool 75 | } 76 | 77 | func (m MultiMatcher) MatchApprox(approx []byte) bool { 78 | for _, im := range m.Inner { 79 | if match := im.MatchApprox(approx); match && !m.All { 80 | return true 81 | } else if !match && m.All { 82 | return false 83 | } 84 | } 85 | 86 | return m.All 87 | } 88 | 89 | func (m MultiMatcher) Match(exact []byte) bool { 90 | for _, im := range m.Inner { 91 | if match := im.MatchApprox(exact) && im.Match(exact); match && !m.All { 92 | return true 93 | } else if !match && m.All { 94 | return false 95 | } 96 | } 97 | 98 | return m.All 99 | } 100 | -------------------------------------------------------------------------------- /matcher_test.go: -------------------------------------------------------------------------------- 1 | package shrek_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/innix/shrek" 9 | ) 10 | 11 | func TestStartEndMatcher_MatchApprox(t *testing.T) { 12 | t.Parallel() 13 | 14 | input := "abcdyjsviqu5fqvqzv5mnfonrapka477vonf6fuko7duolp5g3i" 15 | table := []struct { 16 | Input string 17 | Start string 18 | End string 19 | Match bool 20 | }{ 21 | {Input: input, Start: "abcd", End: "i", Match: true}, 22 | {Input: input, Start: "a", End: "5g3i", Match: true}, 23 | {Input: input, Start: "abcd", End: "5g3i", Match: true}, 24 | {Input: input, Start: "", End: "5g3i", Match: true}, 25 | {Input: input, Start: "abcd", End: "", Match: true}, 26 | {Input: input, Start: "", End: "", Match: true}, 27 | {Input: input, Start: input, End: input, Match: true}, 28 | 29 | {Input: input, Start: "b", End: "z", Match: false}, 30 | {Input: input, Start: "bbb", End: "zzz", Match: false}, 31 | {Input: input, Start: "b", End: "", Match: false}, 32 | {Input: input, Start: "bbb", End: "", Match: false}, 33 | {Input: input, Start: "bbb", End: "i", Match: false}, 34 | {Input: input, Start: "bbb", End: "5g3i", Match: false}, 35 | } 36 | 37 | for _, tc := range table { 38 | tc := tc 39 | name := fmt.Sprintf("%s:%s~=%s", tc.Start, tc.End, tc.Input) 40 | 41 | t.Run(name, func(t *testing.T) { 42 | t.Parallel() 43 | 44 | m := shrek.StartEndMatcher{ 45 | Start: []byte(tc.Start), 46 | End: []byte(tc.End), 47 | } 48 | 49 | if match := m.MatchApprox([]byte(tc.Input)); match != tc.Match { 50 | t.Errorf("invalid match result: got %v, wanted %v", match, tc.Match) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func TestStartEndMatcher_Match(t *testing.T) { 57 | t.Parallel() 58 | 59 | const input = "abcdyjsviqu5fqvqzv5mnfonrapka477vonf6fuko7duolp5g3i" 60 | table := []struct { 61 | Input string 62 | Start string 63 | End string 64 | Match bool 65 | }{ 66 | {Input: input, Start: "abcd", End: "i", Match: true}, 67 | {Input: input, Start: "a", End: "5g3i", Match: true}, 68 | {Input: input, Start: "abcd", End: "5g3i", Match: true}, 69 | {Input: input, Start: "", End: "5g3i", Match: true}, 70 | {Input: input, Start: "abcd", End: "", Match: true}, 71 | {Input: input, Start: "", End: "", Match: true}, 72 | {Input: input, Start: input, End: input, Match: true}, 73 | 74 | {Input: input, Start: "b", End: "z", Match: false}, 75 | {Input: input, Start: "bbb", End: "zzz", Match: false}, 76 | {Input: input, Start: "b", End: "", Match: false}, 77 | {Input: input, Start: "bbb", End: "", Match: false}, 78 | {Input: input, Start: "bbb", End: "i", Match: false}, 79 | {Input: input, Start: "bbb", End: "5g3i", Match: false}, 80 | } 81 | 82 | for _, tc := range table { 83 | tc := tc 84 | name := fmt.Sprintf("%s:%s~=%s", tc.Start, tc.End, tc.Input) 85 | 86 | t.Run(name, func(t *testing.T) { 87 | t.Parallel() 88 | 89 | m := shrek.StartEndMatcher{ 90 | Start: []byte(tc.Start), 91 | End: []byte(tc.End), 92 | } 93 | 94 | if match := m.Match([]byte(tc.Input)); match != tc.Match { 95 | t.Errorf("invalid match result: got %v, wanted %v", match, tc.Match) 96 | } 97 | }) 98 | } 99 | } 100 | 101 | func TestStartEndMatcher_Valid(t *testing.T) { 102 | t.Parallel() 103 | 104 | type row struct { 105 | Start string 106 | End string 107 | Valid bool 108 | } 109 | 110 | // Calculating permutations of all addresses takes way too long. 111 | // const validRunes = "abcdefghijklmnopqrstuvwxyz234567" 112 | const subsetValidRunes = "adiqyz7" // "adipqxyz257" 113 | var table []row 114 | 115 | for _, s := range permutations(t, []rune(subsetValidRunes)) { 116 | // Test End field. 117 | se := s[len(s)-2:] 118 | valid := se == "ad" || se == "id" || se == "qd" || se == "yd" 119 | table = append(table, row{Start: "", End: s, Valid: valid}) 120 | 121 | // Test Start field - these should all be valid. 122 | table = append(table, row{Start: s, End: "", Valid: true}) 123 | } 124 | 125 | // Repeat for uppercase. 126 | for _, s := range permutations(t, []rune(strings.ToUpper(subsetValidRunes))) { 127 | // Test End field - these should all be invalid. 128 | table = append(table, row{Start: "", End: s, Valid: false}) 129 | 130 | // Test Start field - these should all be invalid.. 131 | table = append(table, row{Start: s, End: "", Valid: false}) 132 | } 133 | 134 | // Check search length of filter text. 135 | // An onion address is 56 chars, so anything above 56 is invalid. 136 | 137 | // Check Start length. 138 | table = append(table, 139 | row{Start: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaa", Valid: true}, // len = 56 140 | row{Start: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaa", Valid: false}, // len = 57 141 | row{Start: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaa", Valid: false}, // len = 58 142 | row{Start: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa", Valid: false}, // len = 59 143 | row{Start: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7", Valid: false}, // len = 60 144 | ) 145 | 146 | // Check End length. 147 | table = append(table, 148 | row{End: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaad", Valid: true}, // len = 56 149 | row{End: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaad", Valid: false}, // len = 57 150 | row{End: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaad", Valid: false}, // len = 58 151 | row{End: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaad", Valid: false}, // len = 59 152 | row{End: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaad", Valid: false}, // len = 60 153 | ) 154 | 155 | // Check Start + End length. 156 | table = append(table, 157 | row{Start: "aaaaaaaaa7aaaaaaaaa7aaaaaa", End: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaad", Valid: true}, // len = 56 158 | row{Start: "aaaaaaaaa7aaaaaaaaa7aaaaaaa", End: "aaaaaaaaa7aaaaaaaaa7aaaaaaaad", Valid: true}, // len = 56 159 | row{Start: "aaaaaaaaa7aaaaaaaaa7aaaaaaa", End: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaad", Valid: false}, // len = 57 160 | row{Start: "aaaaaaaaa7", End: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaad", Valid: false}, // len = 57 161 | row{Start: "aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaaaaa7aaaaaad", End: "aaaaaaaaad", Valid: false}, // len = 57 162 | ) 163 | 164 | // Some realistic hand-written test cases, for good measure. 165 | table = append(table, 166 | row{Start: "food", End: "xid", Valid: true}, 167 | row{Start: "food", End: "", Valid: true}, 168 | row{Start: "", End: "xid", Valid: true}, 169 | row{Start: "dark", End: "", Valid: true}, 170 | row{Start: "dark", End: "yd", Valid: true}, 171 | row{Start: "dark", End: "ydd", Valid: false}, 172 | row{Start: "alpine9", End: "", Valid: false}, 173 | row{Start: "alpine2", End: "", Valid: true}, 174 | ) 175 | 176 | for _, tc := range table { 177 | tc := tc 178 | name := fmt.Sprintf("%s:%s=%v", tc.Start, tc.End, tc.Valid) 179 | 180 | t.Run(name, func(t *testing.T) { 181 | t.Parallel() 182 | 183 | m := shrek.StartEndMatcher{ 184 | Start: []byte(tc.Start), 185 | End: []byte(tc.End), 186 | } 187 | 188 | if err := m.Validate(); err == nil && !tc.Valid { 189 | t.Errorf("invalid validation result: wanted: non-nil error, got: nil error") 190 | } else if err != nil && tc.Valid { 191 | t.Errorf("invalid validation result: wanted: nil error, got: %v", err) 192 | } 193 | }) 194 | } 195 | } 196 | 197 | func permutations(t *testing.T, charset []rune) []string { 198 | t.Helper() 199 | 200 | var perms []string 201 | var permFn func(*testing.T, []rune, int) 202 | 203 | permFn = func(t *testing.T, rs []rune, n int) { 204 | t.Helper() 205 | 206 | if n == 1 { 207 | tmp := make([]rune, len(rs)) 208 | copy(tmp, rs) 209 | perms = append(perms, string(tmp)) 210 | return 211 | } 212 | 213 | for i := 0; i < n; i++ { 214 | permFn(t, rs, n-1) 215 | 216 | if n%2 == 1 { 217 | tmp := rs[i] 218 | rs[i] = rs[n-1] 219 | rs[n-1] = tmp 220 | } else { 221 | tmp := rs[0] 222 | rs[0] = rs[n-1] 223 | rs[n-1] = tmp 224 | } 225 | } 226 | } 227 | 228 | permFn(t, charset, len(charset)) 229 | return perms 230 | } 231 | -------------------------------------------------------------------------------- /miner.go: -------------------------------------------------------------------------------- 1 | package shrek 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/innix/shrek/internal/ed25519" 10 | ) 11 | 12 | func MineOnionHostName(ctx context.Context, rand io.Reader, m Matcher) (*OnionAddress, error) { 13 | hostname := make([]byte, EncodedPublicKeySize) 14 | 15 | it, err := ed25519.NewKeyIterator(rand) 16 | if err != nil { 17 | return nil, fmt.Errorf("shrek: could not create key iterator: %w", err) 18 | } 19 | 20 | for more := true; ctx.Err() == nil; more = it.Next() { 21 | if !more { 22 | return nil, errors.New("shrek: searched entire address space and no match was found") 23 | } 24 | 25 | addr := &OnionAddress{ 26 | PublicKey: it.PublicKey(), 27 | 28 | // The private key is not needed to generate the hostname. So to avoid pointless 29 | // computation, we wait until a match has been found first. 30 | SecretKey: nil, 31 | } 32 | 33 | // The approximate encoder only generates the first 51 bytes of the hostname accurately; 34 | // the last 5 bytes are wrong. But it is much faster, so it is used first then the exact 35 | // encoder is used if a match is found here. 36 | addr.HostNameApprox(hostname) 37 | 38 | // Check if approximate hostname matches. 39 | if !m.MatchApprox(hostname) { 40 | continue 41 | } 42 | 43 | // Generate full hostname, so we can check for exact match. Generating the full address 44 | // on every iteration is avoided because it's much slower than the approx. 45 | addr.HostName(hostname) 46 | 47 | // Check if exact hostname matches. 48 | if !m.Match(hostname) { 49 | continue 50 | } 51 | 52 | // Compute private key after a match has been found. 53 | sk, err := it.PrivateKey() 54 | if err != nil { 55 | return nil, fmt.Errorf("shrek: could not compute private key: %w", err) 56 | } 57 | addr.SecretKey = sk 58 | 59 | // Sanity check keys retrieved from iterator. 60 | kp := &ed25519.KeyPair{PublicKey: addr.PublicKey, PrivateKey: addr.SecretKey} 61 | if err := kp.Validate(); err != nil { 62 | return nil, fmt.Errorf("shrek: key validation failed: %w", err) 63 | } 64 | 65 | return addr, nil 66 | } 67 | 68 | return nil, ctx.Err() 69 | } 70 | -------------------------------------------------------------------------------- /onionaddress.go: -------------------------------------------------------------------------------- 1 | package shrek 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base32" 6 | "fmt" 7 | "io" 8 | "io/fs" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/innix/shrek/internal/ed25519" 13 | "golang.org/x/crypto/sha3" 14 | ) 15 | 16 | const ( 17 | // EncodedPublicKeyApproxSize is the size, in bytes, of the public key when 18 | // encoded using the approximate encoder. 19 | EncodedPublicKeyApproxSize = 51 20 | 21 | // EncodedPublicKeySize is the size, in bytes, of the public key when encoded 22 | // using the real encoder. 23 | EncodedPublicKeySize = 56 24 | ) 25 | 26 | const ( 27 | publicKeyFileName = "hs_ed25519_public_key" 28 | secretKeyFileName = "hs_ed25519_secret_key" 29 | hostNameFileName = "hostname" 30 | 31 | publicKeyFileHeader = "== ed25519v1-public: type0 ==\x00\x00\x00" 32 | secretKeyFileHeader = "== ed25519v1-secret: type0 ==\x00\x00\x00" 33 | ) 34 | 35 | var b32 = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding) 36 | 37 | type OnionAddress struct { 38 | PublicKey ed25519.PublicKey 39 | SecretKey ed25519.PrivateKey 40 | } 41 | 42 | // HostName returns the .onion address representation of the public key stored in 43 | // the OnionAddress. The .onion TLD is not included. 44 | func (addr *OnionAddress) HostName(hostname []byte) { 45 | const version = 3 46 | 47 | if l := len(hostname); l != EncodedPublicKeySize { 48 | panic(fmt.Sprintf("bad buffer length: %d", l)) 49 | } 50 | 51 | // checksum = sha3_sum256(".onion checksum" + public_key + version) 52 | var checksumBuf bytes.Buffer 53 | checksumBuf.Write([]byte(".onion checksum")) 54 | checksumBuf.Write(addr.PublicKey) 55 | checksumBuf.Write([]byte{version}) 56 | checksum := sha3.Sum256(checksumBuf.Bytes()) 57 | 58 | // onion_addr = base32_encode(public_key + checksum + version) 59 | var onionAddrBuf bytes.Buffer 60 | onionAddrBuf.Write(addr.PublicKey) 61 | onionAddrBuf.Write(checksum[:2]) 62 | onionAddrBuf.Write([]byte{version}) 63 | 64 | b32.Encode(hostname, onionAddrBuf.Bytes()) 65 | } 66 | 67 | // HostNameString returns the .onion address representation of the public key stored 68 | // in the OnionAddress as a string. Unlike HostName and HostNameApprox, this method 69 | // does include the .onion TLD in the returned hostname. 70 | func (addr *OnionAddress) HostNameString() string { 71 | hostname := make([]byte, EncodedPublicKeySize) 72 | addr.HostName(hostname) 73 | 74 | return fmt.Sprintf("%s.onion", hostname) 75 | } 76 | 77 | // HostNameApprox returns an approximate .onion address representation of the public 78 | // key stored in the OnionAddress. The start of the address is accurate, the last few 79 | // characters at the end are not. The .onion TLD is not included. 80 | func (addr *OnionAddress) HostNameApprox(hostname []byte) { 81 | if l := len(hostname); l != EncodedPublicKeySize { 82 | panic(fmt.Sprintf("bad buffer length: %d", l)) 83 | } 84 | 85 | b32.Encode(hostname, addr.PublicKey) 86 | } 87 | 88 | func GenerateOnionAddress(rand io.Reader) (*OnionAddress, error) { 89 | kp, err := ed25519.GenerateKey(rand) 90 | if err != nil { 91 | return nil, fmt.Errorf("shrek: could not generate onion address: %w", err) 92 | } 93 | 94 | return &OnionAddress{ 95 | PublicKey: kp.PublicKey, 96 | SecretKey: kp.PrivateKey, 97 | }, nil 98 | } 99 | 100 | // SaveOnionAddress saves the hostname, public key, and secret key from the given 101 | // OnionAddress to the destination directory. It creates a sub-directory named after 102 | // the hostname in the destination directory, then it creates 3 files inside the 103 | // created sub-directory: 104 | // 105 | // hs_ed25519_public_key 106 | // hs_ed25519_secret_key 107 | // hostname 108 | // 109 | func SaveOnionAddress(dir string, addr *OnionAddress) error { 110 | const ( 111 | dirMode = 0o700 112 | fileMode = 0o600 113 | ) 114 | 115 | hostname := addr.HostNameString() 116 | dir = filepath.Join(dir, hostname) 117 | 118 | if err := os.MkdirAll(dir, dirMode); err != nil { 119 | return fmt.Errorf("shrek: could not create directories: %w", err) 120 | } 121 | 122 | pkFile := filepath.Join(dir, publicKeyFileName) 123 | pkData := append([]byte(publicKeyFileHeader), addr.PublicKey...) 124 | if err := os.WriteFile(pkFile, pkData, fileMode); err != nil { 125 | return fmt.Errorf("shrek: could not save public key to file: %w", err) 126 | } 127 | 128 | skFile := filepath.Join(dir, secretKeyFileName) 129 | skData := append([]byte(secretKeyFileHeader), addr.SecretKey...) 130 | if err := os.WriteFile(skFile, skData, fileMode); err != nil { 131 | return fmt.Errorf("shrek: could not save secret key to file: %w", err) 132 | } 133 | 134 | hnFile := filepath.Join(dir, hostNameFileName) 135 | hnData := []byte(hostname) 136 | if err := os.WriteFile(hnFile, hnData, fileMode); err != nil { 137 | return fmt.Errorf("shrek: could not save onion hostname to file: %w", err) 138 | } 139 | 140 | return nil 141 | } 142 | 143 | // ReadOnionAddress reads the public key and secret key from the files in the given 144 | // directory, then it parses the keys from the files inside the directory and validates 145 | // that they are valid keys to use as an onion address. 146 | // 147 | // The provided directory must be one created either by the SaveOnionAddress function 148 | // or any other program that outputs the keys in the same format. The directory must 149 | // contain the following files: 150 | // 151 | // hs_ed25519_public_key 152 | // hs_ed25519_secret_key 153 | // 154 | func ReadOnionAddress(dir string) (*OnionAddress, error) { 155 | // Check dir exists. 156 | if fi, err := os.Stat(dir); err != nil && os.IsNotExist(err) { 157 | return nil, fmt.Errorf("shrek: directory not found: %q", dir) 158 | } else if !fi.IsDir() { 159 | return nil, fmt.Errorf("shrek: path is not a directory: %q", dir) 160 | } 161 | 162 | return ReadOnionAddressFS(os.DirFS(dir)) 163 | } 164 | 165 | // ReadOnionAddressFS does the same thing as ReadOnionAddress. The only difference is 166 | // that it accepts an fs.FS to abstract away the underlying file system. 167 | func ReadOnionAddressFS(fsys fs.FS) (*OnionAddress, error) { 168 | // Read public key from file and validate contents. 169 | pkData, err := fs.ReadFile(fsys, publicKeyFileName) 170 | if err != nil { 171 | return nil, fmt.Errorf("shrek: reading public key file: %w", err) 172 | } 173 | if l := len(pkData); l != len(publicKeyFileHeader)+ed25519.PublicKeySize { 174 | return nil, fmt.Errorf("shrek: public key file has wrong length: %d", l) 175 | } 176 | 177 | // Read private key from file and validate contents. 178 | skData, err := fs.ReadFile(fsys, secretKeyFileName) 179 | if err != nil { 180 | return nil, fmt.Errorf("shrek: reading secret key file: %w", err) 181 | } 182 | if l := len(skData); l != len(secretKeyFileHeader)+ed25519.PrivateKeySize { 183 | return nil, fmt.Errorf("shrek: secret key file has wrong length: %d", l) 184 | } 185 | 186 | kp := &ed25519.KeyPair{ 187 | PublicKey: ed25519.PublicKey(pkData[len(publicKeyFileHeader):]), 188 | PrivateKey: ed25519.PrivateKey(skData[len(secretKeyFileHeader):]), 189 | } 190 | 191 | // Validate keys match. 192 | if err := kp.Validate(); err != nil { 193 | return nil, fmt.Errorf("shrek: keys in directory do not match: %w", err) 194 | } 195 | 196 | return &OnionAddress{ 197 | PublicKey: kp.PublicKey, 198 | SecretKey: kp.PrivateKey, 199 | }, nil 200 | } 201 | -------------------------------------------------------------------------------- /onionaddress_test.go: -------------------------------------------------------------------------------- 1 | package shrek_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/innix/shrek" 9 | ) 10 | 11 | const ( 12 | // seed is the seed for the RNG. 13 | seed = "12D345Y678g9X0qwKertIyhOgbnDXmjhgvfdcHxswq12w3De4r5t6y7Vu8i9oT0pmnbKvcxzF" 14 | 15 | // seedHostname is the hostname generated by the RNG using the seed const. 16 | seedHostname = "if62hgkxq6r7c3slwqaj3fhj6in7bcceinhqz7nt7jy6dk77gw4towid" 17 | ) 18 | 19 | var ( 20 | // seedPublicKey is the public key generated by the RNG using the seed const. 21 | seedPublicKey = []byte{ 22 | 65, 125, 163, 153, 87, 135, 163, 241, 110, 75, 180, 0, 157, 148, 233, 242, 23 | 27, 240, 136, 68, 67, 79, 12, 253, 179, 250, 113, 225, 171, 255, 53, 185, 24 | } 25 | 26 | // seedSecretKey is the private key generated by the RNG using the seed const. 27 | seedSecretKey = []byte{ 28 | 224, 52, 184, 160, 72, 18, 130, 195, 179, 118, 143, 220, 68, 119, 107, 106, 29 | 133, 224, 81, 56, 152, 1, 136, 195, 2, 132, 2, 22, 233, 126, 231, 96, 101, 30 | 25, 142, 250, 122, 147, 138, 100, 183, 254, 174, 193, 28, 184, 254, 251, 31 | 154, 205, 94, 104, 106, 84, 161, 23, 92, 126, 34, 187, 241, 101, 129, 69, 32 | } 33 | ) 34 | 35 | func TestOnionAddress_HostName(t *testing.T) { 36 | t.Parallel() 37 | 38 | addr, err := shrek.GenerateOnionAddress(bytes.NewBufferString(seed)) 39 | if err != nil { 40 | t.Fatalf("could not generate the prerequisite onion address: %v", err) 41 | } 42 | 43 | wanted := seedHostname 44 | got := make([]byte, shrek.EncodedPublicKeySize) 45 | addr.HostName(got) 46 | 47 | if string(got) != wanted { 48 | t.Errorf("public key not encoded correctly, got: %q, wanted: %q", got, wanted) 49 | } 50 | } 51 | 52 | func TestOnionAddress_HostName_BadBuffer(t *testing.T) { 53 | t.Parallel() 54 | 55 | addr, err := shrek.GenerateOnionAddress(bytes.NewBufferString(seed)) 56 | if err != nil { 57 | t.Fatalf("could not generate the prerequisite onion address: %v", err) 58 | } 59 | 60 | table := []struct { 61 | ExpectPanic bool 62 | Buffer []byte 63 | }{ 64 | {ExpectPanic: true, Buffer: make([]byte, 0)}, 65 | {ExpectPanic: true, Buffer: make([]byte, shrek.EncodedPublicKeySize-1)}, 66 | {ExpectPanic: true, Buffer: make([]byte, shrek.EncodedPublicKeySize*2)}, 67 | {ExpectPanic: false, Buffer: make([]byte, shrek.EncodedPublicKeySize)}, 68 | } 69 | 70 | for _, tc := range table { 71 | tc := tc 72 | name := fmt.Sprintf("buflen=%d_expectpanic=%v", len(tc.Buffer), tc.ExpectPanic) 73 | 74 | t.Run(name, func(t *testing.T) { 75 | t.Parallel() 76 | 77 | defer func() { 78 | if panicked := recover() != nil; panicked != tc.ExpectPanic { 79 | if tc.ExpectPanic { 80 | t.Error("expected panic, none was detected") 81 | } else { 82 | t.Error("did not expect panic, one was detected") 83 | } 84 | } 85 | }() 86 | 87 | addr.HostName(tc.Buffer) 88 | }) 89 | } 90 | } 91 | 92 | func TestOnionAddress_HostNameString(t *testing.T) { 93 | t.Parallel() 94 | 95 | addr, err := shrek.GenerateOnionAddress(bytes.NewBufferString(seed)) 96 | if err != nil { 97 | t.Fatalf("could not generate the prerequisite onion address: %v", err) 98 | } 99 | 100 | wanted := fmt.Sprintf("%s.onion", seedHostname) 101 | got := addr.HostNameString() 102 | 103 | if got != wanted { 104 | t.Errorf("public key not encoded correctly, got: %q, wanted: %q", got, wanted) 105 | } 106 | } 107 | 108 | func TestOnionAddress_HostNameApprox(t *testing.T) { 109 | t.Parallel() 110 | 111 | addr, err := shrek.GenerateOnionAddress(bytes.NewBufferString(seed)) 112 | if err != nil { 113 | t.Fatalf("could not generate the prerequisite onion address: %v", err) 114 | } 115 | 116 | // Perform several iterations to ensure function is stateless and deterministic. 117 | for i := 0; i < 3; i++ { 118 | wanted := seedHostname 119 | got := make([]byte, shrek.EncodedPublicKeySize) 120 | addr.HostNameApprox(got) 121 | 122 | if string(got[:shrek.EncodedPublicKeyApproxSize]) != wanted[:shrek.EncodedPublicKeyApproxSize] { 123 | t.Errorf("public key not encoded correctly, got: %q, wanted: %q", got, wanted) 124 | } 125 | } 126 | } 127 | 128 | func TestGenerateOnionAddress(t *testing.T) { 129 | t.Parallel() 130 | 131 | addr, err := shrek.GenerateOnionAddress(bytes.NewBufferString(seed)) 132 | if err != nil { 133 | t.Fatalf("could not generate the prerequisite onion address: %v", err) 134 | } 135 | 136 | if !bytes.Equal(addr.PublicKey, seedPublicKey) { 137 | t.Errorf("unexpected public key, got: %v, wanted: %v", addr.PublicKey, seedPublicKey) 138 | } 139 | if !bytes.Equal(addr.SecretKey, seedSecretKey) { 140 | t.Errorf("unexpected secret key, got: %v, wanted: %v", addr.SecretKey, seedSecretKey) 141 | } 142 | } 143 | 144 | func BenchmarkOnionAddress_HostName(b *testing.B) { 145 | addr, err := shrek.GenerateOnionAddress(bytes.NewBufferString(seed)) 146 | if err != nil { 147 | b.Fatalf("could not generate the prerequisite onion address: %v", err) 148 | } 149 | hostname := make([]byte, shrek.EncodedPublicKeySize) 150 | 151 | b.ResetTimer() 152 | for i := 0; i < b.N; i++ { 153 | addr.HostName(hostname) 154 | } 155 | } 156 | 157 | func BenchmarkOnionAddress_HostNameApprox(b *testing.B) { 158 | addr, err := shrek.GenerateOnionAddress(bytes.NewBufferString(seed)) 159 | if err != nil { 160 | b.Fatalf("could not generate the prerequisite onion address: %v", err) 161 | } 162 | hostname := make([]byte, shrek.EncodedPublicKeySize) 163 | 164 | b.ResetTimer() 165 | for i := 0; i < b.N; i++ { 166 | addr.HostNameApprox(hostname) 167 | } 168 | } 169 | 170 | func BenchmarkGenerateOnionAddress(b *testing.B) { 171 | for i := 0; i < b.N; i++ { 172 | _, err := shrek.GenerateOnionAddress(nil) 173 | if err != nil { 174 | b.Fatalf("onion address generator errored unexpectedly during benchmark: %v", err) 175 | } 176 | } 177 | } 178 | --------------------------------------------------------------------------------