/
keychain.py
110 lines (85 loc) · 4 KB
/
keychain.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
import glob
import os.path
import simplejson
from . import crypt_util
from . import padding
from .item import Item
EXPECTED_VERSION = 30645
class AbstractKeychain(object):
"""Implementation of common keychain logic (MP design, etc)."""
def __init__(self, path):
self._open(path)
def _open(self, path):
self.base_path = path
self.check_paths()
self.check_version()
def check_paths(self):
if not(os.path.exists(self.base_path)):
raise ValueError("Proported 1Password keychain %s does not exist" % self.base_path)
def check_version(self):
pass
class AKeychain(AbstractKeychain):
"""Implementation of the classic .agilekeychain storage format"""
def check_paths(self):
super(AKeychain, self).check_paths()
files_to_check = {
'version file': os.path.join(self.base_path, 'config', 'build_num'),
'keys': os.path.join(self.base_path, 'data', 'default', 'encryptionKeys.js')
}
for descriptor, expected_path in files_to_check.iteritems():
if not os.path.exists(expected_path):
raise Exception("Missing %s, expected at %s" % (descriptor, expected_path))
def check_version(self):
super(AKeychain, self).check_version()
version_file = os.path.join(self.base_path, 'config', 'buildnum')
with open(version_file, 'r') as f:
version_num = int(f.read().strip())
if version_num != EXPECTED_VERSION:
raise ValueError("I only understand 1Password build %s" % EXPECTED_VERSION)
def unlock(self, password):
keys = self._load_keys(password)
self._load_items(keys)
def _load_keys(self, password):
self.keys = {}
keys_file = os.path.join(self.base_path, 'data', 'default', 'encryptionKeys.js')
with open(keys_file, 'r') as f:
data = simplejson.load(f)
levels = dict((k, v) for (k, v) in data.iteritems() if k != 'list')
for level, identifier in levels.iteritems():
keys = [k for k in data['list'] if k.get('identifier') == identifier]
assert len(keys) == 1, "There should be exactly one key for level %s, got %d" % (level, len(keysS))
key = keys[0]
self.keys[identifier] = crypt_util.a_decrypt_key(key, password)
self.levels = levels
def _load_items(self, keys):
items = []
for f in glob.glob(os.path.join(self.base_path, 'data', 'default', '*.1password')):
items.append(Item.new_from_file(f, self))
self.items = items
def decrypt(self, keyid, string):
if keyid not in self.keys:
raise ValueError("Item encrypted with unknown key %s" % keyid)
return crypt_util.a_decrypt_item(padding.pkcs5_pad(string), self.keys[keyid])
class CKeychain(AbstractKeychain):
"""Implementation of the modern .cloudkeychain format
Documentation at http://learn.agilebits.com/1Password4/Security/keychain-design.html
"""
INITIAL_KEY_OFFSET=12
KEY_SIZE=32
def check_paths(self):
super(CKeychain, self).check_paths()
files_to_check = {
'profile': os.path.join(self.base_path, 'default', 'profile.js'),
}
for descriptor, expected_path in files_to_check.iteritems():
if not os.path.exists(expected_path):
raise Exception("Missing %s, expected at %s" % (descriptor, expected_path))
def unlock(self, password):
self._load_keys(password)
def _load_keys(self, password):
with open(os.path.join(self.base_path, 'default', 'profile.js'), 'r') as f:
ds = f.read()[self.INITIAL_KEY_OFFSET:-1]
data = simplejson.loads(ds)
super_master_key, hmac_key = crypt_util.opdata1_derive_keys(password, data['salt'], iterations=int(data['iterations']))
master_key = crypt_util.opdata1_decrypt_item(data['masterKey'], super_master_key, hmac_key)
overview_key = crypt_util.opdata1_decrypt_item(data['overviewKey'], super_master_key, hmac_key)