Skip to content

Commit bf175b4

Browse files
committed
gitbrute initial commit.
0 parents  commit bf175b4

File tree

3 files changed

+213
-0
lines changed

3 files changed

+213
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*~

README

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
gitbrute brute-forces a pair of author+committer timestamps such that
2+
the resulting git commit hash your desired prefix.
3+
4+
It will find the most recent time that satisifies your prefix.
5+
6+
Shorter prefixes match more quickly, of course. The author &
7+
committer timestamp are not kept in sync.
8+
9+
Example: https://github.com/bradfitz/deadbeef

gitbrute.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
Copyright 2014 Google Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// The gitbrute command brute-forces a git commit hash prefix.
18+
package main
19+
20+
import (
21+
"bytes"
22+
"crypto/sha1"
23+
"flag"
24+
"fmt"
25+
"log"
26+
"os"
27+
"os/exec"
28+
"regexp"
29+
"runtime"
30+
"strconv"
31+
"strings"
32+
"time"
33+
)
34+
35+
var (
36+
prefix = flag.String("prefix", "bf", "Desired prefix")
37+
force = flag.Bool("force", false, "Re-run, even if current hash matches prefix")
38+
cpu = flag.Int("cpus", runtime.NumCPU(), "Number of CPUs to use. Defaults to number of processors.")
39+
)
40+
41+
var (
42+
start = time.Now()
43+
startUnix = start.Unix()
44+
)
45+
46+
func main() {
47+
flag.Parse()
48+
runtime.GOMAXPROCS(*cpu)
49+
if _, err := strconv.ParseInt(*prefix, 16, 64); err != nil {
50+
log.Fatalf("Prefix %q isn't hex.", *prefix)
51+
}
52+
53+
hash := curHash()
54+
if strings.HasPrefix(hash, *prefix) && !*force {
55+
return
56+
}
57+
58+
obj, err := exec.Command("git", "cat-file", "-p", hash).Output()
59+
if err != nil {
60+
log.Fatal(err)
61+
}
62+
i := bytes.Index(obj, []byte("\n\n"))
63+
if i < 0 {
64+
log.Fatalf("No \\n\\n found in %q", obj)
65+
}
66+
msg := obj[i+2:]
67+
68+
possibilities := make(chan try, 512)
69+
go explore(possibilities)
70+
winner := make(chan solution)
71+
for i := 0; i < *cpu; i++ {
72+
go bruteForce(obj, winner, possibilities)
73+
}
74+
75+
w := <-winner
76+
cmd := exec.Command("git", "commit", "--amend", "--date="+w.author.String(), "--file=-")
77+
cmd.Env = append([]string{"GIT_COMMITTER_DATE=" + w.committer.String()}, os.Environ()...)
78+
cmd.Stdout = os.Stdout
79+
cmd.Stdin = bytes.NewReader(msg)
80+
if err := cmd.Run(); err != nil {
81+
log.Fatalf("amend: %v", err)
82+
}
83+
}
84+
85+
type solution struct {
86+
author, committer date
87+
}
88+
89+
var (
90+
authorDateRx = regexp.MustCompile(`(?m)^author.+> (.+)`)
91+
commiterDateRx = regexp.MustCompile(`(?m)^committer.+> (.+)`)
92+
)
93+
94+
func bruteForce(obj []byte, winner chan<- solution, possibilities <-chan try) {
95+
// blob is the blob to mutate in-place repatedly while testing
96+
// whether we have a match.
97+
blob := []byte(fmt.Sprintf("commit %d\x00%s", len(obj), obj))
98+
authorDate, adatei := getDate(blob, authorDateRx)
99+
commitDate, cdatei := getDate(blob, commiterDateRx)
100+
101+
s1 := sha1.New()
102+
wantHexPrefix := []byte(*prefix)
103+
hexBuf := make([]byte, 0, sha1.Size*2)
104+
105+
for t := range possibilities {
106+
ad := date{startUnix - int64(t.authorBehind), authorDate.tz}
107+
cd := date{startUnix - int64(t.commitBehind), commitDate.tz}
108+
strconv.AppendInt(blob[:adatei], ad.n, 10)
109+
strconv.AppendInt(blob[:cdatei], cd.n, 10)
110+
s1.Reset()
111+
s1.Write(blob)
112+
if !bytes.HasPrefix(hexInPlace(s1.Sum(hexBuf[:0])), wantHexPrefix) {
113+
continue
114+
}
115+
winner <- solution{ad, cd}
116+
return // at least yield one goroutine's CPU for git commit to run.
117+
}
118+
}
119+
120+
// try is a pair of seconds behind now to brute force, looking for a
121+
// matching commit.
122+
type try struct {
123+
commitBehind int
124+
authorBehind int
125+
}
126+
127+
// explore yields the sequence:
128+
// (0, 0)
129+
//
130+
// (0, 1)
131+
// (1, 0)
132+
// (1, 1)
133+
//
134+
// (0, 2)
135+
// (1, 2)
136+
// (2, 0)
137+
// (2, 1)
138+
// (2, 2)
139+
//
140+
// ...
141+
func explore(c chan<- try) {
142+
for max := 0; ; max++ {
143+
for i := 0; i <= max-1; i++ {
144+
c <- try{i, max}
145+
}
146+
for j := 0; j <= max; j++ {
147+
c <- try{max, j}
148+
}
149+
}
150+
}
151+
152+
// date is a git date.
153+
type date struct {
154+
n int64 // unix seconds
155+
tz string
156+
}
157+
158+
func (d date) String() string { return fmt.Sprintf("%d %s", d.n, d.tz) }
159+
160+
// getDate parses out a date from a git header (or blob with a header
161+
// following the size and null byte). It returns the date and index
162+
// that the unix seconds begins at within h.
163+
func getDate(h []byte, rx *regexp.Regexp) (d date, idx int) {
164+
m := rx.FindSubmatchIndex(h)
165+
if m == nil {
166+
log.Fatalf("Failed to match %s in %q", rx, h)
167+
}
168+
v := string(h[m[2]:m[3]])
169+
space := strings.Index(v, " ")
170+
if space < 0 {
171+
log.Fatalf("unexpected date %q", v)
172+
}
173+
n, err := strconv.ParseInt(v[:space], 10, 64)
174+
if err != nil {
175+
log.Fatalf("unexpected date %q", v)
176+
}
177+
return date{n, v[space+1:]}, m[2]
178+
}
179+
180+
func curHash() string {
181+
all, err := exec.Command("git", "show", "--format=format:%H").Output()
182+
if err != nil {
183+
log.Fatal(err)
184+
}
185+
h := string(all)
186+
if i := strings.Index(h, "\n"); i > 0 {
187+
h = h[:i]
188+
}
189+
return h
190+
}
191+
192+
// hexInPlace takes a slice of binary data and returns the same slice with double
193+
// its length, hex-ified in-place.
194+
func hexInPlace(v []byte) []byte {
195+
const hex = "0123456789abcdef"
196+
h := v[:len(v)*2]
197+
for i := len(v) - 1; i >= 0; i-- {
198+
b := v[i]
199+
h[i*2+0] = hex[b>>4]
200+
h[i*2+1] = hex[b&0xf]
201+
}
202+
return h
203+
}

0 commit comments

Comments
 (0)