Skip to content

Commit df11229

Browse files
committed
Merge branch 'feature/improvements' into develop
2 parents 2051039 + 4e65fdf commit df11229

8 files changed

Lines changed: 93 additions & 91 deletions

File tree

.github/dependabot.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ updates:
88
- package-ecosystem: "gomod"
99
directory: "/" # Location of package manifests
1010
schedule:
11-
interval: "monthly"
11+
interval: "semiannually"
1212
- package-ecosystem: "github-actions"
1313
directory: "/" # Location of package manifests
1414
schedule:
15-
interval: "monthly"
15+
interval: "semiannually"

README.md

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ A command-line vanity (public) key generator for [WireGuard](https://www.wiregua
66
and not whether the search matches anywhere in the public key. The concept is based on
77
[wireguard-vanity-address](https://github.com/warner/wireguard-vanity-address), however I wanted something a little more streamlined.
88

9-
109
## Features
1110

1211
- Generates compliant [curve25519](https://cr.yp.to/ecdh.html) private and public keys
@@ -17,7 +16,6 @@ and not whether the search matches anywhere in the public key. The concept is ba
1716
- Exit after results limit reached (defaults to 1)
1817
- Displays probability and estimated runtime based on quick benchmark
1918

20-
2119
## Usage options
2220

2321
```
@@ -29,7 +27,6 @@ Options:
2927
-l, --limit int limit results to n (exists after) (default 1)
3028
```
3129

32-
3330
## Example
3431

3532
```
@@ -42,28 +39,27 @@ Cannot calculate probability for the regular expression "^pc7[/+]"
4239
4340
Press Ctrl-c to cancel
4441
45-
private OFVUjUoTNQp94fNPB9GCLzxiJPTbN03rcDPrVd12uFc= public tEstMXL/3ZzAd2TnVlr1BNs/+eOnKzSHpGUnjspk3kc=
46-
private gInIEDmENYbyuaWR1W/KLfximExwbcCg45W2WOmEc0I= public TestKmA/XVagDW/JsHBXk5mhYJ6E1N1lAWeIeCttgRs=
47-
private yDQLNiQlfnMGhUBsbLQjoBbuNezyHug31Qa1Ht6cgkw= public PC1/3oUId241TLYImJLUObR8NNxz4HXzG4z+EazfWxY=
48-
private QIbJgxy83+F/1kdogcF+T04trs+1N9gAr1t5th2tLXM= public Pc7+h172sx0TfIMikjgszM/B8i8/ghi7qJVOwWQtx0w=
49-
private +CUqn4jcKoL8pw53pD4IzfMKW/IMceDWKcM2W5Dxtn4= public teStmGXZwiJl9HmfnTSmk83girtiIH8oZEa6PFJ8F1Y=
50-
private EMaUfQvAEABpQV/21ALJP5YtyGerRXAn8u67j2AQzVs= public pC1/t2x5V99Y1SBqNgPZDPsa6r+L5y3BJ4XUCJMar3g=
51-
private wNuHOKCfoH1emfvijXNBoc/7KjrEXUeof7tSdGWvRFo= public PC1/jXQosaBad2HePOm/w1KjCZ82eT3qNbfzNDZiwTs=
52-
private gJtn0woDChGvyN2eSdc7mTpAFA/nA6jykJeK5bYYfFA= public Pc7+UEJSHiWsQ9zkO2q+guqDK4sc3VMDMgJu+h/bOFI=
53-
private IMyPmYm/v0SPmB62hC8l6kfxT3/Lfp7dMioo+SM6T2c= public Pc7/uVfD/ZftxWBHwYbaudEywUS61biBcpj5Tw830Q4=
42+
private: OFVUjUoTNQp94fNPB9GCLzxiJPTbN03rcDPrVd12uFc= public: tEstMXL/3ZzAd2TnVlr1BNs/+eOnKzSHpGUnjspk3kc=
43+
private: gInIEDmENYbyuaWR1W/KLfximExwbcCg45W2WOmEc0I= public: TestKmA/XVagDW/JsHBXk5mhYJ6E1N1lAWeIeCttgRs=
44+
private: yDQLNiQlfnMGhUBsbLQjoBbuNezyHug31Qa1Ht6cgkw= public: PC1/3oUId241TLYImJLUObR8NNxz4HXzG4z+EazfWxY=
45+
private: QIbJgxy83+F/1kdogcF+T04trs+1N9gAr1t5th2tLXM= public: Pc7+h172sx0TfIMikjgszM/B8i8/ghi7qJVOwWQtx0w=
46+
private: +CUqn4jcKoL8pw53pD4IzfMKW/IMceDWKcM2W5Dxtn4= public: teStmGXZwiJl9HmfnTSmk83girtiIH8oZEa6PFJ8F1Y=
47+
private: EMaUfQvAEABpQV/21ALJP5YtyGerRXAn8u67j2AQzVs= public: pC1/t2x5V99Y1SBqNgPZDPsa6r+L5y3BJ4XUCJMar3g=
48+
private: wNuHOKCfoH1emfvijXNBoc/7KjrEXUeof7tSdGWvRFo= public: PC1/jXQosaBad2HePOm/w1KjCZ82eT3qNbfzNDZiwTs=
49+
private: gJtn0woDChGvyN2eSdc7mTpAFA/nA6jykJeK5bYYfFA= public: Pc7+UEJSHiWsQ9zkO2q+guqDK4sc3VMDMgJu+h/bOFI=
50+
private: IMyPmYm/v0SPmB62hC8l6kfxT3/Lfp7dMioo+SM6T2c= public: Pc7/uVfD/ZftxWBHwYbaudEywUS61biBcpj5Tw830Q4=
5451
```
5552

56-
5753
## Installing
5854

5955
Download the [latest binary release](https://github.com/axllent/wireguard-vanity-keygen/releases/latest) for your system,
6056
or build from source `go install github.com/axllent/wireguard-vanity-keygen@latest`.
6157

62-
6358
## Timings
6459

6560
To give you a rough idea of how long it will take to generate keys, the following table lists
66-
estimated timings to find a matching key on a system that reported "`Calculating speed: 230,000 calculations per second using 19 CPU cores`" when it started:
61+
estimated timings to find a matching key on a system that reported
62+
"`Calculating speed: 230,000 calculations per second using 19 CPU cores`" when it started:
6763

6864
| Length | Case-insensitive | Case-sensitive |
6965
| :------ | :--------------- | :------------- |
@@ -84,7 +80,6 @@ If any search term contains numbers, the timings would fall somewhere between th
8480

8581
Of course, your mileage will differ, depending on the number, and speed, of your CPU cores.
8682

87-
8883
## Regular Expressions
8984

9085
Since each additional letter in a search term increases the search time exponentially, searching using a regular expression may
@@ -105,8 +100,7 @@ On Windows, you must use double quotes. For example: `"^(a|b)"`.
105100

106101
NOTE: Complex regular expressions, such as those using escape sequences, flags, or character classes, may never match a key.
107102
To avoid that, consider testing your regex using a tool such as [this one](https://go.dev/play/p/6LJy51Wd08O) on The Go Playground,
108-
or the same tool on [goplay.tools](https://goplay.tools/snippet/6LJy51Wd08O).
109-
103+
or the same tool on [goplay.tools](https://goplay.tools/snippet/6LJy51Wd08O).
110104

111105
## FAQ
112106

@@ -116,19 +110,16 @@ Valid characters include `A-Z`, `a-z`, `0-9`, `/` and `+`. There are no other ch
116110

117111
You can also use regex expressions to search.
118112

119-
120113
### Why does `test` & `tes1` show different probabilities despite having 4 characters each?
121114

122115
With case-insensitive searches (default), a-z have the chance of matching both uppercase and lowercase. A search for "cat" can match `Cat`, `cAT` etc.
123116

124-
125117
### How accurate are the estimated times?
126118

127119
They are not (and cannot be) accurate. Keys are completely randomly generated, and the estimate is based on a law of averages.
128120
For instance, you could find a match for a one in a billion chance on the very first hit, or it could take you 5 billion attempts.
129121
It will however give you an indication based on your CPU speed, word count, case sensitivity, and use of numbers or characters.
130122

131-
132123
### Why do I need this?
133124

134125
You don't. I wrote it because I run a WireGuard server, which does not provide any reference as to who the key belongs to (`wg` on the server).

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module github.com/axllent/wireguard-vanity-keygen
33
go 1.24.0
44

55
require (
6+
github.com/oasisprotocol/curve25519-voi v0.0.0-20251114093237-2ab5a27a1729
67
github.com/spf13/pflag v1.0.10
7-
golang.org/x/crypto v0.46.0
88
)
9+
10+
require golang.org/x/sys v0.40.0 // indirect

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
github.com/oasisprotocol/curve25519-voi v0.0.0-20251114093237-2ab5a27a1729 h1:yfQ2sO9WJXUAIUR+g7NUkxJSKCAFJcR5sUDu+ZmjTZI=
2+
github.com/oasisprotocol/curve25519-voi v0.0.0-20251114093237-2ab5a27a1729/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s=
13
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
24
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
3-
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
4-
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
5+
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
6+
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

keygen/crypto.go

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import (
55
"crypto/rand"
66
"encoding/base64"
77

8-
"golang.org/x/crypto/curve25519"
8+
curve25519voi "github.com/oasisprotocol/curve25519-voi/curve"
9+
scalar "github.com/oasisprotocol/curve25519-voi/curve/scalar"
910
)
1011

1112
// KeySize defines the size of the key
@@ -19,33 +20,29 @@ type Key [KeySize]byte
1920
// It is used by WireGuard to represent private keys.
2021
type PrivateKey [KeySize]byte
2122

22-
// NewPrivateKey generates a new curve25519 secret key.
23-
// It conforms to the format described on https://cr.yp.to/ecdh.html.
23+
// NewPrivateKey generates a new curve25519 secret key (clamped).
2424
func newPrivateKey() (PrivateKey, error) {
25-
k, err := newPreSharedKey()
25+
var priv [KeySize]byte
26+
_, err := rand.Read(priv[:])
2627
if err != nil {
2728
return PrivateKey{}, err
2829
}
29-
k[0] &= 248
30-
k[31] = (k[31] & 127) | 64
31-
return (PrivateKey)(*k), nil
30+
// Clamp as per RFC 7748
31+
priv[0] &= 248
32+
priv[31] = (priv[31] & 127) | 64
33+
return PrivateKey(priv), nil
3234
}
3335

34-
// NewPreSharedKey generates a new key
35-
func newPreSharedKey() (*Key, error) {
36-
var k [KeySize]byte
37-
_, err := rand.Read(k[:])
38-
if err != nil {
39-
return nil, err
40-
}
41-
return (*Key)(&k), nil
42-
}
43-
44-
// Public computes the public key matching this curve25519 secret key.
36+
// Public computes the public key matching this curve25519 secret key using curve25519-voi.
4537
func (k *PrivateKey) Public() Key {
46-
var p [KeySize]byte
47-
curve25519.ScalarBaseMult(&p, (*[KeySize]byte)(k))
48-
return (Key)(p)
38+
var pub curve25519voi.MontgomeryPoint
39+
base := *curve25519voi.X25519_BASEPOINT
40+
s, err := scalar.NewFromBytesModOrder(k[:])
41+
if err != nil || s == nil {
42+
panic("invalid private key for scalar.NewFromBytesModOrder: " + err.Error())
43+
}
44+
pub.Mul(&base, s)
45+
return Key(pub)
4946
}
5047

5148
// String returns a private key as a string

keygen/utils.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"time"
1010
)
1111

12-
// regexChars contains the list of the metacharacters used in regular expressions,
12+
// regexChars contains the list of the meta characters used in regular expressions,
1313
// but not including the + character, which is valid in a key
1414
const regexChars = `^$.|?*-[]{}()\`
1515

@@ -31,7 +31,7 @@ func InvalidSearchMsg(s string) string {
3131
func HumanizeDuration(duration time.Duration) string {
3232
// more than duration can handle
3333
if duration.Hours() < 0.0 {
34-
return fmt.Sprintf("hundreds of years")
34+
return "hundreds of years"
3535
}
3636
if duration.Hours() > 8760.0 {
3737
y := int64(duration.Hours() / 8760)
@@ -101,22 +101,22 @@ func NumberFormat(n int64) string {
101101
}
102102
}
103103

104-
// IsRegex returns true if any regex metacharacters (except +) are in the search term
104+
// IsRegex returns true if any regex meta characters (except +) are in the search term
105105
func IsRegex(s string) bool {
106106
return strings.ContainsAny(s, regexChars)
107107
}
108108

109109
// invalidRegexMsg returns an error message how the regex is invalid
110-
func invalidRegexMsg(s string, errmsg string) string {
111-
return fmt.Sprintf("\n\"%s\" is an invalid regular expression\n%s", s, errmsg)
110+
func invalidRegexMsg(s string, errMsg string) string {
111+
return fmt.Sprintf("\n\"%s\" is an invalid regular expression\n%s", s, errMsg)
112112
}
113113

114114
// IsValidRegex checks the regex has any chance of matching a key
115115
func IsValidRegex(s string) string {
116116
// A concise guide on golang's regex syntax is at
117117
// https://pkg.go.dev/regexp/syntax
118118

119-
stripped := removeMetacharacters(s)
119+
stripped := removeMetaCharacters(s)
120120
if !IsValidSearch(stripped) {
121121
return InvalidSearchMsg(s)
122122
}
@@ -188,8 +188,8 @@ func IsValidRegex(s string) string {
188188
return ""
189189
}
190190

191-
// removeMetacharacters removes regex metacharacters (except +) from the string
192-
func removeMetacharacters(s string) string {
191+
// RemoveMetaCharacters removes regex meta characters (except +) from the string
192+
func removeMetaCharacters(s string) string {
193193
// This logic isn't needed anymore, as we don't attempt to calculate the probability of regular expressions
194194
// // remove (?i) from beginning of string
195195
// re := regexp.MustCompile(`^\([^)]*\)`)
@@ -204,7 +204,7 @@ func removeMetacharacters(s string) string {
204204
// re = regexp.MustCompile(`=`)
205205
// s = re.ReplaceAllLiteralString(s, "x")
206206

207-
// strip out remaining regexp metacharacters
207+
// strip out remaining regexp meta characters
208208
for _, rune1 := range regexChars {
209209
s = strings.ReplaceAll(s, string(rune1), "")
210210
}

keygen/worker.go

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"fmt"
55
"regexp"
66
"strings"
7-
"sync"
7+
"sync/atomic"
88
"time"
99
)
1010

@@ -17,16 +17,32 @@ type Options struct {
1717
Timeout string
1818
}
1919

20+
// Cruncher struct
21+
22+
// AtomicCounter struct
23+
type AtomicCounter struct {
24+
Value int64
25+
}
26+
27+
// Dec decrements the counter and returns the new value
28+
func (a *AtomicCounter) Dec() int64 {
29+
return atomic.AddInt64(&a.Value, -1)
30+
}
31+
32+
// Get returns the current value of the counter
33+
func (a *AtomicCounter) Get() int64 {
34+
return atomic.LoadInt64(&a.Value)
35+
}
36+
2037
// Cruncher struct
2138
type Cruncher struct {
2239
Options
23-
WordMap map[string]int
24-
mapMutex sync.RWMutex
25-
RegexpMap map[*regexp.Regexp]int
40+
WordMap map[string]*AtomicCounter
41+
RegexpMap map[*regexp.Regexp]*AtomicCounter
2642
thread chan int
2743
Abort bool // set to true to abort processing
2844
timeout time.Duration
29-
timedout bool
45+
timedOut bool
3046
}
3147

3248
// Pair struct
@@ -39,8 +55,8 @@ type Pair struct {
3955
func New(options Options, timeout time.Duration) *Cruncher {
4056
return &Cruncher{
4157
Options: options,
42-
WordMap: make(map[string]int),
43-
RegexpMap: make(map[*regexp.Regexp]int),
58+
WordMap: make(map[string]*AtomicCounter),
59+
RegexpMap: make(map[*regexp.Regexp]*AtomicCounter),
4460
thread: make(chan int, options.Cores),
4561
timeout: timeout,
4662
}
@@ -60,33 +76,29 @@ func (c *Cruncher) crunch(cb func(match Pair)) bool {
6076
matchKey = strings.ToLower(pub)
6177
}
6278

63-
// Assume the task is completed, once all searched have been found
64-
// and limits have been reached, it sends an exit signal
6579
completed := true
6680

67-
// Allow only one routine at a time to avoid
68-
// "concurrent map iteration and map write"
69-
c.mapMutex.Lock()
70-
defer c.mapMutex.Unlock()
71-
for w, count := range c.WordMap {
72-
if count == 0 {
81+
for w, counter := range c.WordMap {
82+
if counter.Get() <= 0 {
7383
continue
7484
}
7585
completed = false
7686
if strings.HasPrefix(matchKey, w) {
77-
c.WordMap[w] = count - 1
78-
cb(Pair{Private: k.String(), Public: pub})
87+
if counter.Dec() >= 0 {
88+
cb(Pair{Private: k.String(), Public: pub})
89+
}
7990
}
8091
}
8192

82-
for w, count := range c.RegexpMap {
83-
if count == 0 {
93+
for w, counter := range c.RegexpMap {
94+
if counter.Get() <= 0 {
8495
continue
8596
}
8697
completed = false
8798
if w.MatchString(matchKey) {
88-
c.RegexpMap[w] = count - 1
89-
cb(Pair{Private: k.String(), Public: pub})
99+
if counter.Dec() >= 0 {
100+
cb(Pair{Private: k.String(), Public: pub})
101+
}
90102
}
91103
}
92104

@@ -121,8 +133,6 @@ func (c *Cruncher) CalculateSpeed() (int64, time.Duration) {
121133

122134
// Allow only one routine at a time to avoid
123135
// "concurrent map iteration and map write"
124-
c.mapMutex.Lock()
125-
defer c.mapMutex.Unlock()
126136
for w := range c.WordMap {
127137
_ = strings.HasPrefix(t, w)
128138
}
@@ -209,13 +219,13 @@ func (c *Cruncher) Find(cb func(match Pair)) {
209219
}
210220
select {
211221
case <-t.C:
212-
c.timedout = true
222+
c.timedOut = true
213223
c.Abort = true
214224
default:
215225
}
216226
}(t)
217227
if c.Abort {
218-
if c.timedout {
228+
if c.timedOut {
219229
fmt.Printf("Timed out after %v\n", c.timeout)
220230
} else {
221231
if !t.Stop() {

0 commit comments

Comments
 (0)