-
-
Notifications
You must be signed in to change notification settings - Fork 73
/
Copy pathmasterpassword.py
245 lines (209 loc) · 8.69 KB
/
masterpassword.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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# -*- coding: utf-8 -*-
# Inspired by https://raw.githubusercontent.com/xeroc/python-graphenelib/master/graphenestorage/masterpassword.py
import os
import hashlib
import logging
import warnings
from binascii import hexlify
from beemgraphenebase.py23 import py23_bytes
from beemgraphenebase import bip38
from beemgraphenebase.aes import AESCipher
from .exceptions import WrongMasterPasswordException, WalletLocked
log = logging.getLogger(__name__)
class MasterPassword(object):
""" The keys are encrypted with a Masterpassword that is stored in
the configurationStore. It has a checksum to verify correctness
of the password
The encrypted private keys in `keys` are encrypted with a random
**masterkey/masterpassword** that is stored in the configuration
encrypted by the user-provided password.
:param ConfigStore config: Configuration store to get access to the
encrypted master password
"""
def __init__(self, config=None, **kwargs):
if config is None:
raise ValueError("If using encrypted store, a config store is required!")
self.config = config
self.password = None
self.decrypted_master = None
self.config_key = "encrypted_master_password"
@property
def masterkey(self):
""" Contains the **decrypted** master key
"""
return self.decrypted_master
def has_masterpassword(self):
""" Tells us if the config store knows an encrypted masterpassword
"""
return self.config_key in self.config
def locked(self):
""" Is the store locked. E.g. Is a valid password known that can be
used to decrypt the master key?
"""
return not self.unlocked()
def unlocked(self):
""" Is the store unlocked so that I can decrypt the content?
"""
if self.password is not None:
return bool(self.password)
else:
password_storage = self.config["password_storage"]
KEYRING_AVAILABLE = False
if password_storage == "keyring":
try:
import keyring
if not isinstance(keyring.get_keyring(), keyring.backends.fail.Keyring):
KEYRING_AVAILABLE = True
else:
KEYRING_AVAILABLE = False
except ImportError:
KEYRING_AVAILABLE = False
if (
"UNLOCK" in os.environ
and os.environ["UNLOCK"]
and self.config_key in self.config
and self.config[self.config_key]
and password_storage == "environment"
):
log.debug("Trying to use environmental " "variable to unlock wallet")
self.unlock(os.environ.get("UNLOCK"))
return bool(self.password)
elif (
password_storage == "keyring"
and KEYRING_AVAILABLE
and self.config_key in self.config
and self.config[self.config_key]
):
log.debug("Trying to use keyring to unlock wallet")
pwd = keyring.get_password("beem", "wallet")
self.unlock(pwd)
return bool(self.password)
return False
def lock(self):
""" Lock the store so that we can no longer decrypt the content of the
store
"""
self.password = None
self.decrypted_master = None
def unlock(self, password):
""" The password is used to encrypt this masterpassword. To
decrypt the keys stored in the keys database, one must use
BIP38, decrypt the masterpassword from the configuration
store with the user password, and use the decrypted
masterpassword to decrypt the BIP38 encrypted private keys
from the keys storage!
:param str password: Password to use for en-/de-cryption
"""
self.password = password
if self.config_key in self.config and self.config[self.config_key]:
self._decrypt_masterpassword()
else:
self._new_masterpassword(password)
self._save_encrypted_masterpassword()
def wipe_masterpassword(self):
""" Removes the encrypted masterpassword from config storage"""
if self.config_key in self.config and self.config[self.config_key]:
self.config[self.config_key] = None
def _decrypt_masterpassword(self):
""" Decrypt the encrypted masterkey
"""
aes = AESCipher(self.password)
checksum, encrypted_master = self.config[self.config_key].split("$")
try:
decrypted_master = aes.decrypt(encrypted_master)
except Exception:
self._raise_wrongmasterpassexception()
if checksum != self._derive_checksum(decrypted_master):
self._raise_wrongmasterpassexception()
self.decrypted_master = decrypted_master
def _raise_wrongmasterpassexception(self):
self.password = None
raise WrongMasterPasswordException
def _save_encrypted_masterpassword(self):
self.config[self.config_key] = self._get_encrypted_masterpassword()
def _new_masterpassword(self, password):
""" Generate a new random masterkey, encrypt it with the password and
store it in the store.
:param str password: Password to use for en-/de-cryption
"""
# make sure to not overwrite an existing key
if self.config_key in self.config and self.config[self.config_key]:
raise Exception("Storage already has a masterpassword!")
self.decrypted_master = hexlify(os.urandom(32)).decode("ascii")
# Encrypt and save master
self.password = password
self._save_encrypted_masterpassword()
return self.masterkey
def _derive_checksum(self, s):
""" Derive the checksum
:param str s: Random string for which to derive the checksum
"""
checksum = hashlib.sha256(bytes(s, "ascii")).hexdigest()
return checksum[:4]
def _get_encrypted_masterpassword(self):
""" Obtain the encrypted masterkey
.. note:: The encrypted masterkey is checksummed, so that we can
figure out that a provided password is correct or not. The
checksum is only 4 bytes long!
"""
if not self.unlocked():
raise WalletLocked
aes = AESCipher(self.password)
return "{}${}".format(
self._derive_checksum(self.masterkey), aes.encrypt(self.masterkey)
)
def change_password(self, newpassword):
""" Change the password that allows to decrypt the master key
"""
if not self.unlocked():
raise WalletLocked
self.password = newpassword
self._save_encrypted_masterpassword()
def decrypt(self, wif):
""" Decrypt the content according to BIP38
:param str wif: Encrypted key
"""
if not self.unlocked():
raise WalletLocked
return format(bip38.decrypt(wif, self.masterkey), "wif")
def deriveChecksum(self, s):
""" Derive the checksum
"""
checksum = hashlib.sha256(py23_bytes(s, "ascii")).hexdigest()
return checksum[:4]
def encrypt_text(self, txt):
""" Encrypt the content according to BIP38
:param str wif: Unencrypted key
"""
if not self.unlocked():
raise WalletLocked
aes = AESCipher(self.masterkey)
return "{}${}".format(self.deriveChecksum(txt), aes.encrypt(txt))
def decrypt_text(self, enctxt):
""" Decrypt the content according to BIP38
:param str wif: Encrypted key
"""
if not self.unlocked():
raise WalletLocked
aes = AESCipher(self.masterkey)
checksum, encrypted_text = enctxt.split("$")
try:
decrypted_text = aes.decrypt(encrypted_text)
except:
raise WrongMasterPasswordException
if checksum != self.deriveChecksum(decrypted_text):
raise WrongMasterPasswordException
return decrypted_text
def encrypt(self, wif):
""" Encrypt the content according to BIP38
:param str wif: Unencrypted key
"""
if not self.unlocked():
raise WalletLocked
return format(bip38.encrypt(str(wif), self.masterkey), "encwif")
def changePassword(self, newpassword): # pragma: no cover
warnings.warn(
"changePassword will be replaced by change_password in the future",
PendingDeprecationWarning,
)
return self.change_password(newpassword)