package main import ( "bytes" "crypto/sha1" "encoding/hex" "errors" "flag" "fmt" "html/template" "io" "io/ioutil" "log" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "sync" "time" ) func hasSuffix(b, suffix []byte) bool { h := sha1.Sum(b) //log.Printf("%s =~ %s?\n", paddedHex(h[:]), paddedHex(suffix)) return bytes.HasSuffix(h[:], suffix) } func progress(perc float64, size int) string { var ( s = "[" i int d = 100.0 / float64(size) ) for ; i < int(perc/d); i++ { s += "#" } for ; i < size; i++ { s += "-" } s += "]" return fmt.Sprintf("%s %5s%%", s, fmt.Sprintf("%.1f", perc)) } func readUint32(b []byte) uint32 { return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]) } func humanScale(f float64) string { switch { case f > 1e15: return fmt.Sprintf("%.3fP", f/1e15+.0005) case f > 1e12: return fmt.Sprintf("%.3fT", f/1e12+.0005) case f > 1e9: return fmt.Sprintf("%.3fG", f/1e9+.0005) case f > 1e6: return fmt.Sprintf("%.3fM", f/1e6+.0005) case f > 1e3: return fmt.Sprintf("%.3fk", f/1e3+.0005) default: return fmt.Sprintf("%.f", f) } } func findSuffixWorker(suffix, b []byte, ts, min, step uint32, found chan []byte, wg *sync.WaitGroup, round *uint64) { //log.Printf("worker: scan %d down to %d, step %d\n", ts, min, step) for ; ts > min; ts = ts - step { *round++ b[4] = byte(ts >> 24) b[5] = byte(ts >> 16) b[6] = byte(ts >> 8) b[7] = byte(ts) if hasSuffix(b, suffix) { found <- b return } } wg.Done() } func findSuffix(suffix, b []byte, min uint32, workers int) (bool, []byte) { ts := readUint32(b[4:]) wg := new(sync.WaitGroup) var ( round = make([]uint64, workers) found = make(chan []byte, 1) total = ts - min ) for i := 0; i < workers; i++ { wg.Add(1) // Make a copy of the slice, since all workers will manipulate wb := make([]byte, len(b)) copy(wb, b) go findSuffixWorker(suffix, wb, ts-uint32(i), min, uint32(workers), found, wg, &round[i]) } abort := make(chan struct{}, 1) go func() { wg.Wait() abort <- struct{}{} }() timer := time.NewTicker(time.Second) start := time.Now() state := func() string { delta := time.Since(start) var rounds uint64 for _, r := range round { rounds += r } return fmt.Sprintf("%s %s, %sk/s [r=%d,t=%s]\r", time.Now().Format("2006/01/02 15:04:05"), progress((float64(rounds)/float64(total))*100+.05, 20), humanScale(float64(rounds)/(float64(delta)/float64(time.Second))), rounds, delta/time.Second*time.Second) } defer func() { timer.Stop() fmt.Println(state()) }() for { select { case key := <-found: return true, key case <-abort: return false, nil case <-timer.C: os.Stdout.Write([]byte(state())) } } } func readFull(r io.Reader, buf []byte) (n int, err error) { n, err = io.ReadFull(r, buf) if err == io.EOF { err = io.ErrUnexpectedEOF } return } func paddedHex(buf []byte) string { var ( tmp = buf pad string ) for i := 0; i < len(tmp); i += 2 { pad += strings.ToUpper(hex.EncodeToString(tmp[i:i+2])) + " " } return pad[:len(pad)-1] } func readableFingerprint(packet []byte) string { hash := sha1.Sum(packet) return paddedHex(hash[:]) } func readPublicKeyPacket(packet []byte, offset int) (err error) { //log.Printf("offset: %d\n", offset) //log.Printf("packet:\n%s\n", hex.Dump(packet[:16])) if version := packet[offset]; version != 4 { return fmt.Errorf("unsupported public key version %d", version) } switch keyType := packet[offset+5]; keyType { case 1: log.Println("public key type RSA") case 2: log.Println("public key type RSA (encrypt only)") case 3: log.Println("public key type RSA (sign only)") case 16: err = errors.New("unsupported ElGamal public key") return case 17: err = errors.New("unsupported DSA public key (you suck!)") return case 18: err = errors.New("unsupported ECDH public key") return case 19: err = errors.New("unsupported ECDSA public key") return default: err = fmt.Errorf("unknown public key %#02x", keyType) return } log.Printf("public key %s\n", readableFingerprint(packet)) return } func readPacket(r io.Reader) (packet []byte, tag byte, offset int, err error) { packet = make([]byte, 1, 4096) if _, err = readFull(r, packet); err != nil { return } if packet[0]&0x80 == 0 { err = errors.New("tag byte does not have MSB set") return } if packet[0]&0x40 == 0 { tag = (packet[0] & 0x3f) >> 2 lengthType := packet[0] & 3 if lengthType == 3 { err = errors.New("packet contains no data") return } lengthBytes := 1 << lengthType //log.Printf("got %d length bytes\n", lengthBytes) packet = append(packet, make([]byte, lengthBytes)...) _, err = readFull(r, packet[1:]) if err != nil { return } //log.Printf("packet: %q (%d)\n", packet, len(packet)) var length int for i := 0; i < lengthBytes; i++ { length <<= 8 length |= int(packet[1+i]) } offset = lengthBytes + 1 packet = append(packet, make([]byte, length)...) _, err = readFull(r, packet[lengthBytes+1:]) return } err = errors.New("new packet not supported") return } var ( keyRingTemplate, _ = template.New("keyring").Parse(`Key-Type: RSA Key-Length: 4096 Subkey-Type: RSA Subkey-Length: 1024 Name-Real: {{.Name}} Name-Email: {{.Email}} Expire-Date: 0 %pubring {{.PubRing}} %secring {{.SecRing}} %commit %echo done`) ) func populateStdin(str string) func(io.WriteCloser) { return func(stdin io.WriteCloser) { defer stdin.Close() io.Copy(stdin, bytes.NewBufferString(str)) } } func gpgVersion() (float64, error) { b, err := exec.Command("gpg", "--version").Output() if err != nil { return 0, err } const prefix = "gpg (GnuPG) " if !strings.HasPrefix(string(b), prefix) { return 0, nil } return strconv.ParseFloat(string(b[len(prefix):len(prefix)+3]), 32) } func generateKeyRing(bits int, name, email string) (secRing, pubRing []byte, err error) { var secFile, pubFile *os.File if secFile, err = ioutil.TempFile("", "secring"); err != nil { return } secFile.Close() if pubFile, err = ioutil.TempFile("", "pubring"); err != nil { return } pubFile.Close() defer func() { os.Remove(secFile.Name()) os.Remove(pubFile.Name()) }() log.Printf("generating %d bits RSA key for %s <%s>\n", bits, name, email) cmd := exec.Command("gpg", "--batch", "--gen-key") cmd.Stderr = os.Stderr var stdin io.WriteCloser if stdin, err = cmd.StdinPipe(); err != nil { return } if err = cmd.Start(); err != nil { return } if err = keyRingTemplate.Execute(stdin, map[string]interface{}{ "Bits": bits, "Name": name, "Email": email, "PubRing": pubFile.Name(), "SecRing": secFile.Name(), }); err != nil { return } if err = stdin.Close(); err != nil { return } if err = cmd.Wait(); err != nil { log.Panic(err) } if secRing, err = ioutil.ReadFile(secFile.Name()); err != nil { return } if l := len(secRing); l < 1024 { err = fmt.Errorf("keypair generation failed: unlikely secring size %d", l) return } if pubRing, err = ioutil.ReadFile(pubFile.Name()); err != nil { return } if l := len(pubRing); l < 1024 { err = fmt.Errorf("keypair generation failed: unlikely secring size %d", l) return } return } func saveRing(name string, pubRing, secRing []byte, ts uint32, offset, length int, uidString string) (err error) { log.Printf("saving dir %s/\n", name) if err = os.MkdirAll(name, 0700); err != nil { return } log.Printf("found match on %s\n", time.Unix(int64(ts), 0)) log.Printf("replace timestamp at +%d\n", offset) pubRing[4] = byte(ts >> 24) pubRing[5] = byte(ts >> 16) pubRing[6] = byte(ts >> 8) pubRing[7] = byte(ts) pubName := filepath.Join(name, "pubring.gpg") log.Printf("saving pubring to %s\n", pubName) if err = ioutil.WriteFile(pubName, pubRing, 0644); err != nil { return } secRing[4] = byte(ts >> 24) secRing[5] = byte(ts >> 16) secRing[6] = byte(ts >> 8) secRing[7] = byte(ts) secName := filepath.Join(name, "secring.gpg") log.Printf("saving secring to %s\n", secName) if err = ioutil.WriteFile(secName, secRing, 0600); err != nil { return } return } func find(workers, bits int, name, email string, suffix []byte, min time.Duration) (found bool, err error) { var secRing, pubRing []byte if secRing, pubRing, err = generateKeyRing(bits, name, email); err != nil { return } ringFile := bytes.NewBuffer(pubRing) var ( packet []byte tag byte offset int ) if packet, tag, offset, err = readPacket(ringFile); err != nil { log.Fatalln(err) } log.Printf("read %d bytes %#02x (%#02x) packet, offset %d\n", len(packet), tag, packet[0], offset) if !(tag == 0x05 || tag == 0x06) { log.Fatalf("packet not a private key or public key, got %#02x\n", tag) } if err = readPublicKeyPacket(packet, offset); err != nil { log.Fatalln(err) } tsMin := time.Now().Add(-min) log.Printf("scanning for suffix %s up until %s\n", paddedHex(suffix), tsMin) var ( key []byte start = time.Now() ) if found, key = findSuffix(suffix, packet, uint32(tsMin.Unix()), workers); found { delta := time.Since(start) fingerprint := readableFingerprint(key) log.Printf("public key %s found in %s\n", fingerprint, delta) uid := fmt.Sprintf("%s <%s>", name, email) err = saveRing(strings.Replace(fingerprint, " ", "", -1), pubRing, secRing, readUint32(key[4:]), offset, len(packet), uid) } return } func main() { suffixString := flag.String("suffix", "", "suffix to find") scanMin := flag.String("min", "43800h", "minimal valid timestamp") bits := flag.Int("bits", 4096, "RSA key size") name := flag.String("name", "John Doe", "name in uid") email := flag.String("email", "john.doe@example.org", "email in uid") workers := flag.Int("workers", (runtime.NumCPU()*3)/2, "number of workers") flag.Parse() if *suffixString == "" { log.Fatalln("supply a -suffix") } suffix, err := hex.DecodeString(*suffixString) if err != nil { log.Fatalf("invalid suffix %q: %v\n", *suffixString, err) } min, err := time.ParseDuration(*scanMin) if err != nil { log.Fatalf("invalid duration %q: %v\n", *scanMin, err) } tmp, err := ioutil.TempDir("", "pgp-suffix-finder") if err != nil { log.Fatalln("unable to create temporary folder: %v\n", err) } defer os.RemoveAll(tmp) if err = os.Mkdir(filepath.Join(tmp, ".gnupg"), 0700); err != nil { log.Fatalln("unable to create temporary folder: %v\n", err) } os.Setenv("GNUPGHOME", filepath.Join(tmp, ".gnupg")) ver, err := gpgVersion() if err != nil { log.Fatalf("failed to probe GnuPG version: %v\n", err) } if ver == 0.0 { log.Println("WARNING! unknown GnuPG version") } else if ver > 2.0 { log.Printf("WARNING! unsupported GnuPG version %.1f.x; tested up until 2.0.30\n", ver) } else { log.Printf("GnuPG version %.1f.x\n", ver) } for { var found bool if found, err = find(*workers, *bits, *name, *email, suffix, min); err != nil { log.Fatalln(err) } if found { break } log.Println("... not found, next round!") } }