diff --git a/.gitignore b/.gitignore index 7a804c6..45696a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /dist/ /wireguard-vanity-keygen +/wireguard-vanity-keygen.exe /cmd/wg-vanity-keygen/wg-vanity-keygen diff --git a/README.md b/README.md index 61ceb0c..6cfda96 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A command-line vanity (public) key generator for [WireGuard](https://www.wiregua - Generates compliant [curve25519](https://cr.yp.to/ecdh.html) private and public keys - Configurable multi-core processing (defaults to all cores) - Optional case sensitive searching +- Optional regex searching - Search multiple prefixes at once - Exit after results limit reached (defaults to 1) - Displays probability and estimated runtime based on quick benchmark @@ -30,25 +31,26 @@ Options: ## Example ``` -$ wireguard-vanity-keygen -l 4 test pc1/ +$ wireguard-vanity-keygen -l 3 test pc1/ "^pc7[+/]" Calculating speed: 49,950 calculations per second using 4 CPU cores Case-insensitive search, exiting after 4 results Probability for "test": 1 in 2,085,136 (approx 41 seconds per match) Probability for "pc1/": 1 in 5,914,624 (approx 1 minute per match) +Cannot calculate probability for the regular expression "^pc7[/+]" Press Ctrl-c to cancel private OFVUjUoTNQp94fNPB9GCLzxiJPTbN03rcDPrVd12uFc= public tEstMXL/3ZzAd2TnVlr1BNs/+eOnKzSHpGUnjspk3kc= private gInIEDmENYbyuaWR1W/KLfximExwbcCg45W2WOmEc0I= public TestKmA/XVagDW/JsHBXk5mhYJ6E1N1lAWeIeCttgRs= private yDQLNiQlfnMGhUBsbLQjoBbuNezyHug31Qa1Ht6cgkw= public PC1/3oUId241TLYImJLUObR8NNxz4HXzG4z+EazfWxY= +private QIbJgxy83+F/1kdogcF+T04trs+1N9gAr1t5th2tLXM= public Pc7+h172sx0TfIMikjgszM/B8i8/ghi7qJVOwWQtx0w= private +CUqn4jcKoL8pw53pD4IzfMKW/IMceDWKcM2W5Dxtn4= public teStmGXZwiJl9HmfnTSmk83girtiIH8oZEa6PFJ8F1Y= -private 2G0X+IvBLw3NRfRnHb8diIXp96NQ9wSu4gdqPidy3nw= public tESt3DBU40Q/Zkp0d1aeb6HOgEOsEM3BxzNqLckKhhc= private EMaUfQvAEABpQV/21ALJP5YtyGerRXAn8u67j2AQzVs= public pC1/t2x5V99Y1SBqNgPZDPsa6r+L5y3BJ4XUCJMar3g= private wNuHOKCfoH1emfvijXNBoc/7KjrEXUeof7tSdGWvRFo= public PC1/jXQosaBad2HePOm/w1KjCZ82eT3qNbfzNDZiwTs= -private 8IdcNsman/ZRGvqWzw1e5cRfhhdtAAmk02X9TkQxhHI= public pC1/N8coOcXmcwO09QXxLrF5/BoHQfvp/qsysGPXiw0= +private gJtn0woDChGvyN2eSdc7mTpAFA/nA6jykJeK5bYYfFA= public Pc7+UEJSHiWsQ9zkO2q+guqDK4sc3VMDMgJu+h/bOFI= +private IMyPmYm/v0SPmB62hC8l6kfxT3/Lfp7dMioo+SM6T2c= public Pc7/uVfD/ZftxWBHwYbaudEywUS61biBcpj5Tw830Q4= ``` - ## Timings 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: | 8 chars | 7 months | 38 years | | 9 chars | 22 years | 175 years | -Note that the above timings are for finding a result for any search term. -Passing multiple search terms will not substantially increase the time, +Note that the above timings are for finding a result for any search term. +Passing multiple search terms will not substantially increase the time, but increasing the limit to two (`--limit 2`) will double the estimated time, three will triple the time, etc. If any search term contains numbers, the timings would fall somewhere between the case-insensitive and case-sensitive columns. Of course, your mileage will differ, depending on the number, and speed, of your CPU cores. +## Regular Expressions + +Since each additional letter in a search term increases the search time exponentially, searching using a regular expression may +reduce the time considerably. Here are some examples: + +1. `.*word.*` - find word anywhere in the key (`word.*` and `.*word` will also work) +2. `^.{0,10}word` - find word anywhere in the first 10 letters of the key +3. `word1.*word2` - find two words, anywhere in the key +4. `^[s5][o0][ll]ar` - find 'solar', or the visually similar 's01ar`, at the beginning of the key +5. `^(best|next)[/+]` - find 'best', or the 'next' best, at the beginning of the key, with `/` or `+` as a delimiter + +A good guide on Go's regular expression syntax is at https://pkg.go.dev/regexp/syntax. + +To include a `+` in your regular expression, preface it with a backslash, like `\+`. + +NOTE: If your search term contains shell metacharacters, such as `|`, or `^`, you will need to quote it. +On Windows, you must use double quotes. For example: `"^(a|b)"`. + +NOTE: Complex regular expressions, such as those using escape sequences, flags, or character classes, may never match a key. +To avoid that, consider testing your regex using a tool such as [this one](https://go.dev/play/p/6LJy51Wd08O). + ## Installing 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 Valid characters include `A-Z`, `a-z`, `0-9`, `/` and `+`. There are no other characters in a hash. +You can also use regex expressions to search. ### Why does `test` & `tes1` show different probabilities despite having 4 characters each? diff --git a/keygen/utils.go b/keygen/utils.go index a2f7e7e..8198d8b 100644 --- a/keygen/utils.go +++ b/keygen/utils.go @@ -5,15 +5,28 @@ import ( "math" "regexp" "strconv" + "strings" "time" ) +// regexChars contains the list of regex metacharacters, excluding +, +// which is valid in a key +const regexChars = `^$.|?*-[]{}()\` + +// regexWillNeverMatch is a shared error message that the regex will never match +const regexWillNeverMatch = "The regular expression will never match" + // IsValidSearch checks the search does not contain any invalid characters func IsValidSearch(s string) bool { var r = regexp.MustCompile(`[^a-zA-Z0-9\/\+]`) return !r.MatchString(s) } +// InvalidSearchMsg returns the error message the search term contains invalid characters +func InvalidSearchMsg(s string) string { + return fmt.Sprintf("\n\"%s\" contains invalid characters\nValid characters include letters [a-z], numbers [0-9], + and /", s) +} + // HumanizeDuration returns a human-readable output of time.Duration func HumanizeDuration(duration time.Duration) string { // more than duration can handle @@ -87,3 +100,111 @@ func NumberFormat(n int64) string { } } } + +// IsRegex returns true if any regex metacharacters (except +) are in the search term +func IsRegex(s string) bool { + return strings.ContainsAny(s, regexChars) +} + +// invalidRegexMsg returns an error message how the regex is invalid +func invalidRegexMsg(s string, errmsg string) string { + return fmt.Sprintf("\n\"%s\" is an invalid regular expression\n%s", s, errmsg) +} + +// IsValidRegex checks the regex has any chance of matching a key +func IsValidRegex(s string) string { + // A consise guide on golang's regex syntax is at + // https://pkg.go.dev/regexp/syntax + + stripped := removeMetacharacters(s) + if !IsValidSearch(stripped) { + return InvalidSearchMsg(s) + } + + // Expressions with '^' character + re := regexp.MustCompile(`.\^`) + if re.MatchString(s) { + return invalidRegexMsg(s, "The '^' character must appear at the beginning of the search term") + } + + // Expressions with '$' character + re = regexp.MustCompile(`\$.`) + if re.MatchString(s) { + return invalidRegexMsg(s, "The '$' character must appear at the end of the search term") + } + re = regexp.MustCompile(`[^=]\$`) + if re.MatchString(s) { + return invalidRegexMsg(s, "A search at the end of the string must contain an '=' character, as all keys end with an `=`") + } + re = regexp.MustCompile(`=[^$]`) + if re.MatchString(s) { + return invalidRegexMsg(s, "The '=' character can only appear at the end of a key") + } + // The command: + // wireguard-vanity-keygen -l 1000 . | grep private | cut -c 105- | sort -u | tr -d "=" | tr -d "\n" + // outputs: + // 048AEIMQUYcgkosw + re = regexp.MustCompile(`[^048AEIMQUYcgkosw]=\$`) + if re.MatchString(s) { + return invalidRegexMsg(s, regexWillNeverMatch) + } + + // Expressions with backslashes: + + // A regex of just a backslash and a single character will never match + re = regexp.MustCompile(`^\\.$`) + if re.MatchString(s) { + return invalidRegexMsg(s, regexWillNeverMatch) + } + + // Control characters and many octal values will meter match, disallow them all + re = regexp.MustCompile(`\\[aftnrxswWpP0-7]`) + if re.MatchString(s) { + return invalidRegexMsg(s, regexWillNeverMatch) + } + + // Disallow backslashes followed by any non-alnum or + character + re = regexp.MustCompile(`\\[^A-Za-z0-9+]`) + if re.MatchString(s) { + return invalidRegexMsg(s, regexWillNeverMatch) + } + + // Expressions with character classes: [[:alnum:]], etc. + + // [[:blank:]], [[:cntrl:]], [[:punct:]] and [[:space:]] will never match + re = regexp.MustCompile(`\[\[:(blank|cntrl|punct|space):\]\]`) + if re.MatchString(s) { + return invalidRegexMsg(s, regexWillNeverMatch) + } + + // [[^:ascii:]], [[^:graph:]], [[^:print:]] will never match + re = regexp.MustCompile(`\[\[\^:(ascii|graph|print):\]\]`) + if re.MatchString(s) { + return invalidRegexMsg(s, regexWillNeverMatch) + } + + return "" +} + +// removeMetacharacters removes regex metacharacters from the string +func removeMetacharacters(s string) string { + // This logic isn't needed anymore, as we don't attempt to calculate the probability of regular expressions + // // remove (?i) from beginning of string + // re := regexp.MustCompile(`^\([^)]*\)`) + // s = re.ReplaceAllLiteralString(s, "") + // // replace [a-b]+ with x + // re = regexp.MustCompile(`\[[^]]*\]\+?`) + // s = re.ReplaceAllLiteralString(s, "x") + // // strip all {n} + // re = regexp.MustCompile(`\{[^}]+\}`) + // s = re.ReplaceAllLiteralString(s, "") + // // replace = with x + // re = regexp.MustCompile(`=`) + // s = re.ReplaceAllLiteralString(s, "x") + + // strip out remaining regexp metacharacters + for _, rune1 := range []rune(regexChars) { + s = strings.ReplaceAll(s, string(rune1), "") + } + return s +} diff --git a/keygen/worker.go b/keygen/worker.go index 20e9525..4b20f7a 100644 --- a/keygen/worker.go +++ b/keygen/worker.go @@ -1,6 +1,7 @@ package keygen import ( + "regexp" "strings" "sync" "time" @@ -17,10 +18,11 @@ type Options struct { // Cruncher struct type Cruncher struct { Options - WordMap map[string]int - mapMutex sync.RWMutex - thread chan int - Abort bool // set to true to abort processing + WordMap map[string]int + mapMutex sync.RWMutex + RegexpMap map[*regexp.Regexp]int + thread chan int + Abort bool // set to true to abort processing } // Pair struct @@ -32,9 +34,10 @@ type Pair struct { // New returns a Cruncher func New(options Options) *Cruncher { return &Cruncher{ - Options: options, - WordMap: make(map[string]int), - thread: make(chan int, options.Cores), + Options: options, + WordMap: make(map[string]int), + RegexpMap: make(map[*regexp.Regexp]int), + thread: make(chan int, options.Cores), } } @@ -71,6 +74,17 @@ func (c *Cruncher) crunch(cb func(match Pair)) bool { } } + for w, count := range c.RegexpMap { + if count == 0 { + continue + } + completed = false + if w.MatchString(matchKey) { + c.RegexpMap[w] = count - 1 + cb(Pair{Private: k.String(), Public: pub}) + } + } + <-c.thread // removes an int from threads, allowing another to proceed return completed } @@ -107,6 +121,11 @@ func (c *Cruncher) CalculateSpeed() (int64, time.Duration) { for w := range c.WordMap { _ = strings.HasPrefix(t, w) } + + for w := range c.RegexpMap { + _ = w.MatchString(t) + } + <-c.thread // removes an int from threads, allowing another to proceed n++ }() diff --git a/main.go b/main.go index c5d9a6e..8eb08d3 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "regexp" "runtime" "strings" "time" @@ -75,23 +76,53 @@ func main() { cs, options.LimitResults, keygen.Plural("result", int64(options.LimitResults))) for _, word := range args { + word = strings.Trim(word, " ") sword := word - if !options.CaseSensitive { - sword = strings.ToLower(sword) + if !keygen.IsRegex(sword) { + if !keygen.IsValidSearch(sword) { + fmt.Fprintln(os.Stderr, keygen.InvalidSearchMsg(word)) + os.Exit(2) + } + if !options.CaseSensitive { + sword = strings.ToLower(sword) + } + c.WordMap[sword] = options.LimitResults + probability := keygen.CalculateProbability(sword, options.CaseSensitive) + estimate64 := int64(speed) * probability + estimate := time.Duration(estimate64) + + fmt.Printf("Probability for \"%s\": 1 in %s (approx %s per match)\n", + word, keygen.NumberFormat(probability), keygen.HumanizeDuration(estimate)) + continue } - if !keygen.IsValidSearch(sword) { - fmt.Printf("\n\"%s\" contains invalid characters\n", word) - fmt.Println("Valid characters include letters [a-z], numbers [0-9], + and /") + + errmsg := keygen.IsValidRegex(sword) + if errmsg != "" { + fmt.Fprintln(os.Stderr, errmsg) os.Exit(2) } - c.WordMap[sword] = options.LimitResults - probability := keygen.CalculateProbability(sword, options.CaseSensitive) - estimate64 := int64(speed) * probability - estimate := time.Duration(estimate64) + fmt.Printf("Probability for \"%s\" cannot be calculated as it is a regular expression\n", sword) - fmt.Printf("Probability for \"%s\": 1 in %s (approx %s per match)\n", - word, keygen.NumberFormat(probability), keygen.HumanizeDuration(estimate)) + // strip off leading .* as it's implied: + re := regexp.MustCompile(`^\.\*`) + sword = re.ReplaceAllLiteralString(sword, "") + // strip off trailing .* as it's implied: + re = regexp.MustCompile(`\.\*$`) + sword = re.ReplaceAllLiteralString(sword, "") + + regex := sword + if !options.CaseSensitive { + if !strings.HasPrefix(regex, "(?i)") { + regex = "(?i)" + regex + } + } + re, err := regexp.Compile(regex) + if err != nil { + fmt.Fprintf(os.Stderr, "\n\"%s\" is an invalid regular expression: %v\n", word, err) + os.Exit(2) + } + c.RegexpMap[re] = options.LimitResults } fmt.Printf("\nPress Ctrl-c to cancel\n\n")