Skip to content

Commit 6ff2d42

Browse files
authored
Add regex support (#13)
* Add regex support * feat: strip out {n}s * fix: ignore binary on Windows * fix: Unlock after loop, not function end As defer defers to the end of the function, not the end of the block. See https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01 * fix: add (?i) prefix for case-insensitive regexes * Skip calc for regexs, simplying loop logic * go fmt * Replace MustCompile with Compile * Don't include added (?i) in error message * Add regex section to readme, tweak regex error message * Reject regexes that have no chance of finding a match Also: Output error messages to stderr Only add (?i) to regex if not already there Move regex logic to end of utils.go Trim spaces from search term * docs: Add link to regex tester, add escaping +s
1 parent 4820321 commit 6ff2d42

5 files changed

Lines changed: 220 additions & 24 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/dist/
22
/wireguard-vanity-keygen
3+
/wireguard-vanity-keygen.exe
34
/cmd/wg-vanity-keygen/wg-vanity-keygen

README.md

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ A command-line vanity (public) key generator for [WireGuard](https://www.wiregua
1010
- Generates compliant [curve25519](https://cr.yp.to/ecdh.html) private and public keys
1111
- Configurable multi-core processing (defaults to all cores)
1212
- Optional case sensitive searching
13+
- Optional regex searching
1314
- Search multiple prefixes at once
1415
- Exit after results limit reached (defaults to 1)
1516
- Displays probability and estimated runtime based on quick benchmark
@@ -30,25 +31,26 @@ Options:
3031
## Example
3132

3233
```
33-
$ wireguard-vanity-keygen -l 4 test pc1/
34+
$ wireguard-vanity-keygen -l 3 test pc1/ "^pc7[+/]"
3435
Calculating speed: 49,950 calculations per second using 4 CPU cores
3536
Case-insensitive search, exiting after 4 results
3637
Probability for "test": 1 in 2,085,136 (approx 41 seconds per match)
3738
Probability for "pc1/": 1 in 5,914,624 (approx 1 minute per match)
39+
Cannot calculate probability for the regular expression "^pc7[/+]"
3840
3941
Press Ctrl-c to cancel
4042
4143
private OFVUjUoTNQp94fNPB9GCLzxiJPTbN03rcDPrVd12uFc= public tEstMXL/3ZzAd2TnVlr1BNs/+eOnKzSHpGUnjspk3kc=
4244
private gInIEDmENYbyuaWR1W/KLfximExwbcCg45W2WOmEc0I= public TestKmA/XVagDW/JsHBXk5mhYJ6E1N1lAWeIeCttgRs=
4345
private yDQLNiQlfnMGhUBsbLQjoBbuNezyHug31Qa1Ht6cgkw= public PC1/3oUId241TLYImJLUObR8NNxz4HXzG4z+EazfWxY=
46+
private QIbJgxy83+F/1kdogcF+T04trs+1N9gAr1t5th2tLXM= public Pc7+h172sx0TfIMikjgszM/B8i8/ghi7qJVOwWQtx0w=
4447
private +CUqn4jcKoL8pw53pD4IzfMKW/IMceDWKcM2W5Dxtn4= public teStmGXZwiJl9HmfnTSmk83girtiIH8oZEa6PFJ8F1Y=
45-
private 2G0X+IvBLw3NRfRnHb8diIXp96NQ9wSu4gdqPidy3nw= public tESt3DBU40Q/Zkp0d1aeb6HOgEOsEM3BxzNqLckKhhc=
4648
private EMaUfQvAEABpQV/21ALJP5YtyGerRXAn8u67j2AQzVs= public pC1/t2x5V99Y1SBqNgPZDPsa6r+L5y3BJ4XUCJMar3g=
4749
private wNuHOKCfoH1emfvijXNBoc/7KjrEXUeof7tSdGWvRFo= public PC1/jXQosaBad2HePOm/w1KjCZ82eT3qNbfzNDZiwTs=
48-
private 8IdcNsman/ZRGvqWzw1e5cRfhhdtAAmk02X9TkQxhHI= public pC1/N8coOcXmcwO09QXxLrF5/BoHQfvp/qsysGPXiw0=
50+
private gJtn0woDChGvyN2eSdc7mTpAFA/nA6jykJeK5bYYfFA= public Pc7+UEJSHiWsQ9zkO2q+guqDK4sc3VMDMgJu+h/bOFI=
51+
private IMyPmYm/v0SPmB62hC8l6kfxT3/Lfp7dMioo+SM6T2c= public Pc7/uVfD/ZftxWBHwYbaudEywUS61biBcpj5Tw830Q4=
4952
```
5053

51-
5254
## Timings
5355

5456
To give you a rough idea of how long it will take to generate keys, the following table lists
@@ -64,14 +66,35 @@ estimated timings for each match on a system that reported "`Calculating speed:
6466
| 8 chars | 7 months | 38 years |
6567
| 9 chars | 22 years | 175 years |
6668

67-
Note that the above timings are for finding a result for any search term.
68-
Passing multiple search terms will not substantially increase the time,
69+
Note that the above timings are for finding a result for any search term.
70+
Passing multiple search terms will not substantially increase the time,
6971
but increasing the limit to two (`--limit 2`) will double the estimated time, three will triple the time, etc.
7072

7173
If any search term contains numbers, the timings would fall somewhere between the case-insensitive and case-sensitive columns.
7274

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

77+
## Regular Expressions
78+
79+
Since each additional letter in a search term increases the search time exponentially, searching using a regular expression may
80+
reduce the time considerably. Here are some examples:
81+
82+
1. `.*word.*` - find word anywhere in the key (`word.*` and `.*word` will also work)
83+
2. `^.{0,10}word` - find word anywhere in the first 10 letters of the key
84+
3. `word1.*word2` - find two words, anywhere in the key
85+
4. `^[s5][o0][ll]ar` - find 'solar', or the visually similar 's01ar`, at the beginning of the key
86+
5. `^(best|next)[/+]` - find 'best', or the 'next' best, at the beginning of the key, with `/` or `+` as a delimiter
87+
88+
A good guide on Go's regular expression syntax is at https://pkg.go.dev/regexp/syntax.
89+
90+
To include a `+` in your regular expression, preface it with a backslash, like `\+`.
91+
92+
NOTE: If your search term contains shell metacharacters, such as `|`, or `^`, you will need to quote it.
93+
On Windows, you must use double quotes. For example: `"^(a|b)"`.
94+
95+
NOTE: Complex regular expressions, such as those using escape sequences, flags, or character classes, may never match a key.
96+
To avoid that, consider testing your regex using a tool such as [this one](https://go.dev/play/p/6LJy51Wd08O).
97+
7598
## Installing
7699

77100
Download the [latest binary release](https://github.com/axllent/wireguard-vanity-keygen/releases/latest) for your system,
@@ -84,6 +107,7 @@ or build from source `go install github.com/axllent/wireguard-vanity-keygen@late
84107

85108
Valid characters include `A-Z`, `a-z`, `0-9`, `/` and `+`. There are no other characters in a hash.
86109

110+
You can also use regex expressions to search.
87111

88112
### Why does `test` & `tes1` show different probabilities despite having 4 characters each?
89113

keygen/utils.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,28 @@ import (
55
"math"
66
"regexp"
77
"strconv"
8+
"strings"
89
"time"
910
)
1011

12+
// regexChars contains the list of regex metacharacters, excluding +,
13+
// which is valid in a key
14+
const regexChars = `^$.|?*-[]{}()\`
15+
16+
// regexWillNeverMatch is a shared error message that the regex will never match
17+
const regexWillNeverMatch = "The regular expression will never match"
18+
1119
// IsValidSearch checks the search does not contain any invalid characters
1220
func IsValidSearch(s string) bool {
1321
var r = regexp.MustCompile(`[^a-zA-Z0-9\/\+]`)
1422
return !r.MatchString(s)
1523
}
1624

25+
// InvalidSearchMsg returns the error message the search term contains invalid characters
26+
func InvalidSearchMsg(s string) string {
27+
return fmt.Sprintf("\n\"%s\" contains invalid characters\nValid characters include letters [a-z], numbers [0-9], + and /", s)
28+
}
29+
1730
// HumanizeDuration returns a human-readable output of time.Duration
1831
func HumanizeDuration(duration time.Duration) string {
1932
// more than duration can handle
@@ -87,3 +100,111 @@ func NumberFormat(n int64) string {
87100
}
88101
}
89102
}
103+
104+
// IsRegex returns true if any regex metacharacters (except +) are in the search term
105+
func IsRegex(s string) bool {
106+
return strings.ContainsAny(s, regexChars)
107+
}
108+
109+
// 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)
112+
}
113+
114+
// IsValidRegex checks the regex has any chance of matching a key
115+
func IsValidRegex(s string) string {
116+
// A consise guide on golang's regex syntax is at
117+
// https://pkg.go.dev/regexp/syntax
118+
119+
stripped := removeMetacharacters(s)
120+
if !IsValidSearch(stripped) {
121+
return InvalidSearchMsg(s)
122+
}
123+
124+
// Expressions with '^' character
125+
re := regexp.MustCompile(`.\^`)
126+
if re.MatchString(s) {
127+
return invalidRegexMsg(s, "The '^' character must appear at the beginning of the search term")
128+
}
129+
130+
// Expressions with '$' character
131+
re = regexp.MustCompile(`\$.`)
132+
if re.MatchString(s) {
133+
return invalidRegexMsg(s, "The '$' character must appear at the end of the search term")
134+
}
135+
re = regexp.MustCompile(`[^=]\$`)
136+
if re.MatchString(s) {
137+
return invalidRegexMsg(s, "A search at the end of the string must contain an '=' character, as all keys end with an `=`")
138+
}
139+
re = regexp.MustCompile(`=[^$]`)
140+
if re.MatchString(s) {
141+
return invalidRegexMsg(s, "The '=' character can only appear at the end of a key")
142+
}
143+
// The command:
144+
// wireguard-vanity-keygen -l 1000 . | grep private | cut -c 105- | sort -u | tr -d "=" | tr -d "\n"
145+
// outputs:
146+
// 048AEIMQUYcgkosw
147+
re = regexp.MustCompile(`[^048AEIMQUYcgkosw]=\$`)
148+
if re.MatchString(s) {
149+
return invalidRegexMsg(s, regexWillNeverMatch)
150+
}
151+
152+
// Expressions with backslashes:
153+
154+
// A regex of just a backslash and a single character will never match
155+
re = regexp.MustCompile(`^\\.$`)
156+
if re.MatchString(s) {
157+
return invalidRegexMsg(s, regexWillNeverMatch)
158+
}
159+
160+
// Control characters and many octal values will meter match, disallow them all
161+
re = regexp.MustCompile(`\\[aftnrxswWpP0-7]`)
162+
if re.MatchString(s) {
163+
return invalidRegexMsg(s, regexWillNeverMatch)
164+
}
165+
166+
// Disallow backslashes followed by any non-alnum or + character
167+
re = regexp.MustCompile(`\\[^A-Za-z0-9+]`)
168+
if re.MatchString(s) {
169+
return invalidRegexMsg(s, regexWillNeverMatch)
170+
}
171+
172+
// Expressions with character classes: [[:alnum:]], etc.
173+
174+
// [[:blank:]], [[:cntrl:]], [[:punct:]] and [[:space:]] will never match
175+
re = regexp.MustCompile(`\[\[:(blank|cntrl|punct|space):\]\]`)
176+
if re.MatchString(s) {
177+
return invalidRegexMsg(s, regexWillNeverMatch)
178+
}
179+
180+
// [[^:ascii:]], [[^:graph:]], [[^:print:]] will never match
181+
re = regexp.MustCompile(`\[\[\^:(ascii|graph|print):\]\]`)
182+
if re.MatchString(s) {
183+
return invalidRegexMsg(s, regexWillNeverMatch)
184+
}
185+
186+
return ""
187+
}
188+
189+
// removeMetacharacters removes regex metacharacters from the string
190+
func removeMetacharacters(s string) string {
191+
// This logic isn't needed anymore, as we don't attempt to calculate the probability of regular expressions
192+
// // remove (?i) from beginning of string
193+
// re := regexp.MustCompile(`^\([^)]*\)`)
194+
// s = re.ReplaceAllLiteralString(s, "")
195+
// // replace [a-b]+ with x
196+
// re = regexp.MustCompile(`\[[^]]*\]\+?`)
197+
// s = re.ReplaceAllLiteralString(s, "x")
198+
// // strip all {n}
199+
// re = regexp.MustCompile(`\{[^}]+\}`)
200+
// s = re.ReplaceAllLiteralString(s, "")
201+
// // replace = with x
202+
// re = regexp.MustCompile(`=`)
203+
// s = re.ReplaceAllLiteralString(s, "x")
204+
205+
// strip out remaining regexp metacharacters
206+
for _, rune1 := range []rune(regexChars) {
207+
s = strings.ReplaceAll(s, string(rune1), "")
208+
}
209+
return s
210+
}

keygen/worker.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package keygen
22

33
import (
4+
"regexp"
45
"strings"
56
"sync"
67
"time"
@@ -17,10 +18,11 @@ type Options struct {
1718
// Cruncher struct
1819
type Cruncher struct {
1920
Options
20-
WordMap map[string]int
21-
mapMutex sync.RWMutex
22-
thread chan int
23-
Abort bool // set to true to abort processing
21+
WordMap map[string]int
22+
mapMutex sync.RWMutex
23+
RegexpMap map[*regexp.Regexp]int
24+
thread chan int
25+
Abort bool // set to true to abort processing
2426
}
2527

2628
// Pair struct
@@ -32,9 +34,10 @@ type Pair struct {
3234
// New returns a Cruncher
3335
func New(options Options) *Cruncher {
3436
return &Cruncher{
35-
Options: options,
36-
WordMap: make(map[string]int),
37-
thread: make(chan int, options.Cores),
37+
Options: options,
38+
WordMap: make(map[string]int),
39+
RegexpMap: make(map[*regexp.Regexp]int),
40+
thread: make(chan int, options.Cores),
3841
}
3942
}
4043

@@ -71,6 +74,17 @@ func (c *Cruncher) crunch(cb func(match Pair)) bool {
7174
}
7275
}
7376

77+
for w, count := range c.RegexpMap {
78+
if count == 0 {
79+
continue
80+
}
81+
completed = false
82+
if w.MatchString(matchKey) {
83+
c.RegexpMap[w] = count - 1
84+
cb(Pair{Private: k.String(), Public: pub})
85+
}
86+
}
87+
7488
<-c.thread // removes an int from threads, allowing another to proceed
7589
return completed
7690
}
@@ -107,6 +121,11 @@ func (c *Cruncher) CalculateSpeed() (int64, time.Duration) {
107121
for w := range c.WordMap {
108122
_ = strings.HasPrefix(t, w)
109123
}
124+
125+
for w := range c.RegexpMap {
126+
_ = w.MatchString(t)
127+
}
128+
110129
<-c.thread // removes an int from threads, allowing another to proceed
111130
n++
112131
}()

main.go

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"fmt"
55
"os"
6+
"regexp"
67
"runtime"
78
"strings"
89
"time"
@@ -75,23 +76,53 @@ func main() {
7576
cs, options.LimitResults, keygen.Plural("result", int64(options.LimitResults)))
7677

7778
for _, word := range args {
79+
word = strings.Trim(word, " ")
7880
sword := word
79-
if !options.CaseSensitive {
80-
sword = strings.ToLower(sword)
81+
if !keygen.IsRegex(sword) {
82+
if !keygen.IsValidSearch(sword) {
83+
fmt.Fprintln(os.Stderr, keygen.InvalidSearchMsg(word))
84+
os.Exit(2)
85+
}
86+
if !options.CaseSensitive {
87+
sword = strings.ToLower(sword)
88+
}
89+
c.WordMap[sword] = options.LimitResults
90+
probability := keygen.CalculateProbability(sword, options.CaseSensitive)
91+
estimate64 := int64(speed) * probability
92+
estimate := time.Duration(estimate64)
93+
94+
fmt.Printf("Probability for \"%s\": 1 in %s (approx %s per match)\n",
95+
word, keygen.NumberFormat(probability), keygen.HumanizeDuration(estimate))
96+
continue
8197
}
82-
if !keygen.IsValidSearch(sword) {
83-
fmt.Printf("\n\"%s\" contains invalid characters\n", word)
84-
fmt.Println("Valid characters include letters [a-z], numbers [0-9], + and /")
98+
99+
errmsg := keygen.IsValidRegex(sword)
100+
if errmsg != "" {
101+
fmt.Fprintln(os.Stderr, errmsg)
85102
os.Exit(2)
86103
}
87-
c.WordMap[sword] = options.LimitResults
88104

89-
probability := keygen.CalculateProbability(sword, options.CaseSensitive)
90-
estimate64 := int64(speed) * probability
91-
estimate := time.Duration(estimate64)
105+
fmt.Printf("Probability for \"%s\" cannot be calculated as it is a regular expression\n", sword)
92106

93-
fmt.Printf("Probability for \"%s\": 1 in %s (approx %s per match)\n",
94-
word, keygen.NumberFormat(probability), keygen.HumanizeDuration(estimate))
107+
// strip off leading .* as it's implied:
108+
re := regexp.MustCompile(`^\.\*`)
109+
sword = re.ReplaceAllLiteralString(sword, "")
110+
// strip off trailing .* as it's implied:
111+
re = regexp.MustCompile(`\.\*$`)
112+
sword = re.ReplaceAllLiteralString(sword, "")
113+
114+
regex := sword
115+
if !options.CaseSensitive {
116+
if !strings.HasPrefix(regex, "(?i)") {
117+
regex = "(?i)" + regex
118+
}
119+
}
120+
re, err := regexp.Compile(regex)
121+
if err != nil {
122+
fmt.Fprintf(os.Stderr, "\n\"%s\" is an invalid regular expression: %v\n", word, err)
123+
os.Exit(2)
124+
}
125+
c.RegexpMap[re] = options.LimitResults
95126
}
96127

97128
fmt.Printf("\nPress Ctrl-c to cancel\n\n")

0 commit comments

Comments
 (0)