├── .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 |
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 |67 | 68 | Check out the project on GitHub. 69 | 70 |
71 | 72 |"}, 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 | --------------------------------------------------------------------------------