From 0d2fe320a7eb2afe3aa0d3fef8d44e9c2a93b980 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Sun, 17 Apr 2016 20:42:26 +0000 Subject: [PATCH] cmd/stvanity: New utility to create vanity device IDs A potential practical use is to encode a short version of the hostname at the beginning of the device ID. For example: jb@syno:~/s/g/s/s/c/stvanity $ stvanity abc Want 15 bits for prefix "ABC", about 3.3e+04 certs to test (statistically speaking) Found ABCFPWS-JKDIFV3-E5IUAQW-DK53WVR-HY7XWBS-56H33GR-CJQI67Q-VGXRMAW Saved to cert.pem, key.pem jb@syno:~/s/g/s/s/c/stvanity $ stvanity $(hostname) Want 20 bits for prefix "SYNO", about 1e+06 certs to test (statistically speaking) Trying 554 certs/s, tested 8307 so far in 15s, expect ~32m total time to complete Trying 543 certs/s, tested 16277 so far in 30s, expect ~32m total time to complete ... The rest is just a matter of patience. jb@syno:~/s/g/s/s/c/stvanity $ stvanity syncthing Want 50 bits for prefix "SYNCTHI-NG", about 1.1e+15 certs to test (statistically speaking) Trying 529 certs/s, tested 7941 so far in 15s, expect ~67443 years total time to complete ... GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2986 --- cmd/stvanity/main.go | 212 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 cmd/stvanity/main.go diff --git a/cmd/stvanity/main.go b/cmd/stvanity/main.go new file mode 100644 index 000000000..5cbfa6b49 --- /dev/null +++ b/cmd/stvanity/main.go @@ -0,0 +1,212 @@ +// Copyright (C) 2016 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package main + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "flag" + "fmt" + "math/big" + mr "math/rand" + "os" + "runtime" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/syncthing/syncthing/lib/protocol" +) + +type result struct { + id protocol.DeviceID + priv *ecdsa.PrivateKey + derBytes []byte +} + +func main() { + flag.Parse() + prefix := strings.ToUpper(strings.Replace(flag.Arg(0), "-", "", -1)) + if len(prefix) > 7 { + prefix = prefix[:7] + "-" + prefix[7:] + } + + found := make(chan result) + stop := make(chan struct{}) + var count int64 + + // Print periodic progress reports. + go printProgress(prefix, &count) + + // Run one certificate generator per CPU core. + var wg sync.WaitGroup + for i := 0; i < runtime.GOMAXPROCS(-1); i++ { + wg.Add(1) + go func() { + generatePrefixed(prefix, &count, found, stop) + wg.Done() + }() + } + + // Save the result, when one has been found. + res := <-found + close(stop) + wg.Wait() + + fmt.Println("Found", res.id) + saveCert(res.priv, res.derBytes) + fmt.Println("Saved to cert.pem, key.pem") +} + +// Try certificates until one is found that has the prefix at the start of +// the resulting device ID. Increments count atomically, sends the result to +// found, returns when stop is closed. +func generatePrefixed(prefix string, count *int64, found chan<- result, stop <-chan struct{}) { + notBefore := time.Now() + notAfter := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC) + + template := x509.Certificate{ + SerialNumber: new(big.Int).SetInt64(mr.Int63()), + Subject: pkix.Name{ + CommonName: "syncthing", + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + + priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + for { + select { + case <-stop: + return + default: + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + id := protocol.NewDeviceID(derBytes) + atomic.AddInt64(count, 1) + + if strings.HasPrefix(id.String(), prefix) { + select { + case found <- result{id, priv, derBytes}: + case <-stop: + } + return + } + } +} + +func printProgress(prefix string, count *int64) { + started := time.Now() + wantBits := 5 * len(prefix) + if wantBits > 63 { + fmt.Printf("Want %d bits for prefix %q, refusing to boil the ocean.\n", wantBits, prefix) + os.Exit(1) + } + expectedIterations := float64(int(1) << uint(wantBits)) + fmt.Printf("Want %d bits for prefix %q, about %.2g certs to test (statistically speaking)\n", wantBits, prefix, expectedIterations) + + for _ = range time.NewTicker(15 * time.Second).C { + tried := atomic.LoadInt64(count) + elapsed := time.Since(started) + rate := float64(tried) / elapsed.Seconds() + expected := timeStr(expectedIterations / rate) + fmt.Printf("Trying %.0f certs/s, tested %d so far in %v, expect ~%s total time to complete\n", rate, tried, elapsed/time.Second*time.Second, expected) + } +} + +func saveCert(priv interface{}, derBytes []byte) { + certOut, err := os.Create("cert.pem") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + err = certOut.Close() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + block, err := pemBlockForKey(priv) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + err = pem.Encode(keyOut, block) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + err = keyOut.Close() + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func pemBlockForKey(priv interface{}) (*pem.Block, error) { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil + case *ecdsa.PrivateKey: + b, err := x509.MarshalECPrivateKey(k) + if err != nil { + return nil, err + } + return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil + default: + return nil, fmt.Errorf("unknown key type") + } +} + +func timeStr(seconds float64) string { + if seconds < 60 { + return fmt.Sprintf("%.0fs", seconds) + } + if seconds < 3600 { + return fmt.Sprintf("%.0fm", seconds/60) + } + if seconds < 86400 { + return fmt.Sprintf("%.0fh", seconds/3600) + } + if seconds < 86400*365 { + return fmt.Sprintf("%.0f days", seconds/3600) + } + return fmt.Sprintf("%.0f years", seconds/86400/365) +}