Hugo Peixoto

Brute forcing my own passphrase

Published on November 20, 2020

Back in May, when I was cleaning up the server that runs git.hugopeixoto.net, I found an ssh key whose passphrase I couldn’t remember. It’s an old key that I created 10 years ago to use in my employer’s laptop. I don’t work there anymore and I don’t actively use this key, so it’s not like I lost access to anything. I have another user on this server whose passphrase I remember. It still bothered me, though. When I first found out about the missing passphrase, I made a list of potential passphrases with variants for capitalization, punctuation and things like that, and tried them all in a python script, but it got me nowhere.

From time to time, I remember a potential passphrase and try it. Today, I remembered a new one to try, so I added it to the list. I was feeling kind of lazy and didn’t want to generate every possible combination of punctuation/capitalization/etc, so I wrote an expansion function that works a bit like bash’s {a,b} syntax. The full script looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def expand(x):
    state = 0
    parts = [[""]]

    for f in x:
        if f == '{' and state == 0:
            state = 1
            parts.append([""])
        elif f == '}' and state == 1:
            state = 0
            parts[-1] = parts[-1][0].split(",")
            parts.append([""])
        else:
            parts[-1][0] += f

    def rec(parts, current, results):
        if parts:
            for part in parts[0]:
                rec(parts[1:], current + part, results)
        else:
            results.append(current)

    results = []
    rec(parts, "", results)

    return results

def expandall(*strings):
    ret = []
    for x in strings:
        ret += expand(x)

    return ret

# I used to have a manually generated list here
passphrases = expandall(
  "{f,F}irst template",
  "s{e,E}cond template",
)

print(len(passphrases))
for passphrase in passphrases:
  if subprocess.run([
    "ssh-keygen", "-yf", "/path/to/privatekey", "-P", passphrase
  ]).returncode == 0:
    print(passphrase)
    break

I could probably use a flatmap or a nested comprehension list in expandall, but I just wanted this to work. Using variable names like f and x is a good indication that I was in “just write some code” mode. I don’t even know why I fall back to python for these kinds of scripts, instead of going with ruby. Maybe it’s because when I’m writing ruby I automatically go into “let’s write this in a single chain of method calls” mode?

Anyway, if you feed expand something like "{w,W}hy{ ,}python{?,?!}", it would generate a list of 2*2*2=8 strings:

Nothing fancy, but it saves me some work. Before testing the new passphrase candidate, I changed the existing list to use the expand function, to reduce the file size and try some new combinations. The list had ~100 manual passphrase variations, stemming from ~10 templates. After converting them to use expand, I ended up with ~1000 variations. These would take some time to run, but why not? I launched the process and…

It found the answer after ~50 attempts!

It found the answer as it was iterating variations of the second passphrase template, so I did know the passphrase… mostly. I tried connecting to the server using that key, entered the passphrase, and it worked. It’s kind of stupid but I was super excited when the script found the answer, even if it’s useless. This problem lived in the back of my head for six months, and I was a bit worried and frustrated by the fact that I had no idea what the passphrase could be. I’m happy that I can take this out of my mind.