-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathpassphrase.py
executable file
·208 lines (180 loc) · 6.08 KB
/
passphrase.py
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/usr/bin/env python
# theymos (Michael Marquardt)
# Public domain
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
from passlib.hash import sha512_crypt
from passlib.utils import ab64_encode, ab64_decode
from hashlib import sha512
import sys
import json
import unicodedata
import getpass
import getopt
PASSPHRASE_ROUNDS = 1000000
salt_questions = [
'The full name you were given as a child',
'4-digit year in which you were born',
'2-digit month in which you were born',
'2-digit date in which you were born',
'City where you were born (do not include state, country, etc.)',
'Other salt (not recommended)'
]
# verify answers to salt questions to prevent confusion about exact formats
salt_verification = [
lambda x: True,
lambda x: len(x) == 0 or (len(x) == 4 and x.isdigit()),
lambda x: len(x) == 0 or (len(x) == 2 and x.isdigit()),
lambda x: len(x) == 0 or (len(x) == 2 and x.isdigit()),
lambda x: True,
lambda x: True,
]
# Hash passphrase using salt
def generate_key( passphrase, salt ):
assert len(passphrase) > 0
# add salt to passphrase and to sha512_crypt's salt
crypt_salt = ab64_encode(sha512(salt).digest())[:16]
passphrase += salt
assert len(crypt_salt) == 16
key = sha512_crypt.encrypt(passphrase, rounds = PASSPHRASE_ROUNDS, salt = crypt_salt)
assert len(key) > 86
# get just the hash back
key = ab64_decode(key[-86:])
assert len(key) == 64
return key
# Expand or truncate key to bytes
def expand_key ( key, bytes ):
if len(key) >= bytes:
return key[:bytes]
out = ''
i = 0
while len(out) < bytes:
out += sha512(key + str(i)).digest()
i += 1
return out[:bytes]
def in_prompt ( prompt ):
sys.stderr.write(prompt)
return sys.stdin.readline().rstrip('\n')
def help ():
print >> sys.stderr, """
Securely computes a key from a passphrase and salt.
-b --output-bytes=N Number of bytes to output. The default is 32,
the number of bytes in a Bitcoin private key.
-s --salt-file=FILE The file to save salt info in. The default is
./salt. An empty string or '/dev/null' will
disable writing a salt file.
-e --echo Echo the passphrase.
--help Show this help.
"""
def input_normalized( prompt, echo = True):
if echo:
answer = in_prompt(prompt)
else:
answer = getpass.getpass(prompt)
answer = unicode(answer, sys.stdin.encoding)
answer = answer.strip()
answer = unicodedata.normalize('NFC', answer)
return answer
try:
opts = getopt.getopt(sys.argv[1:], 'b:s:o:e', ['output-bytes=', 'salt-file=', 'output-format=', 'help', 'echo'])[0]
except getopt.GetoptError as err:
print >> sys.stderr, "Invalid option\n"
help()
sys.exit(2)
# defaults
output_bytes = 32
salt_file = './salt'
echo = False
for o, a in opts:
if o == '--help':
help()
sys.exit(2)
elif o in ('-b', '--output-bytes'):
output_bytes = int(a)
elif o in ('-s', '--salt-file'):
salt_file = a
elif o in ('-e', '--echo'):
echo = True
write_salt = True
if salt_file in ('', '/dev/null'):
write_salt = False
# read salt file if it exists
salt = None
if write_salt:
try:
salt = json.loads(open(salt_file, 'r').read())
for q in range(0, len(salt_questions)):
if salt[q][0] != salt_questions[q]:
raise Exception()
have_salt_file = True
except:
salt = None
have_salt_file = False
if salt is None:
print >> sys.stderr, """========SALT========
Your answers to these questions will be added to your passphrase
in order to make attacks more difficult. It should be impossible
for you to forget your answers to these questions. You are not
trying to give answers that are difficult to guess. If you don't
want to answer any of the questions, leave them blank.
"""
salt = []
for q in range(0, len(salt_questions)):
answer = input_normalized(salt_questions[q] + ": ")
answer = answer.lower()
if not salt_verification[q](answer):
print >> sys.stderr, "Invalid answer"
sys.exit(2)
salt.append([salt_questions[q], answer])
print >> sys.stderr, """
========WALLET ID========
Optionally, you may use this field to create multiple wallets
from one passphrase and salt. For example, you could set this to
'personal' to get a key for a personal wallet and 'work' to get
a key for a work wallet.
"""
wallet_id = input_normalized("Wallet ID: ")
wallet_id = wallet_id.lower()
print >> sys.stderr, """
========PASSPHRASE========
A passphrase for Bitcoin must be much stronger than passphrases
for banks, etc. because everyone on Earth can and will try to
guess your passphrase.
"""
# Everything in salt/passphrase should be encoded as UTF-8
passphrase = input_normalized("Passphrase: ", echo)
if not passphrase:
print >> sys.stderr, "Need a passphrase"
sys.exit(2)
passphrase = passphrase.encode('utf-8')
crypt_salt = ''
print >> sys.stderr, "\n(everything but passphrase converted to lowercase)"
for q in salt:
print >> sys.stderr, q[0] + ': <<' + q[1] + '>>'
crypt_salt += q[1].encode('utf-8')
crypt_salt += wallet_id.encode('utf-8')
print >> sys.stderr, "Wallet ID: <<" + wallet_id + ">>"
if echo:
print >> sys.stderr, "Passphrase: <<" + passphrase.decode('utf-8') + ">>"
print >> sys.stderr, "Salt: <<" + crypt_salt.decode('utf-8') + ">>"
print >> sys.stderr, "Passphrase (hex): <<" + passphrase.encode('hex') + ">>"
print >> sys.stderr, "Salt (hex): <<" + crypt_salt.encode('hex') + ">>"
print >> sys.stderr, "\nCalculating key..."
key = expand_key(generate_key(passphrase, crypt_salt), output_bytes).encode('hex')
print >> sys.stderr, "Final key:"
print key
#Write salt
if write_salt and not have_salt_file:
print >> sys.stderr, "Writing salt to '" + salt_file + "' ..."
try:
open(salt_file, 'w').write(json.dumps(salt))
except Exception as e:
print >> sys.stderr, "Writing salt failed: " + e.strerror
else:
print >> sys.stderr, "OK"