Permalink
Browse files

mostly-working support for cloudkeychain key derivation

  • Loading branch information...
1 parent d16914f commit d246ba6f17e9b82d742c6b8c60b4200c2fb89cbb @Roguelazer committed Feb 19, 2013
View
@@ -6,3 +6,4 @@
* item creation
* unit tests
* `.cloudkeychain` support
+ * reimplement pbkdf2 in C, because the python PBKDF2 module is slow
@@ -4,6 +4,8 @@
import struct
import Crypto.Cipher.AES
+import Crypto.Hash.MD5
+import Crypto.Hash.SHA512
import pbkdf2
from . import padding
@@ -75,6 +77,31 @@ def unhexize(hex_string):
return ''.join(chr(i) for i in res)
+def pbkdf1(key, salt=None, key_size=16, rounds=2, hash_algo=Crypto.Hash.MD5, count=1):
+ """Reimplement the simple PKCS#5 v1.5 key derivation function from OpenSSL
+
+ (as in `openssl enc`). Technically, this is only PBKDF1 if the key size is
+ 20 bytes or less. But whatever.
+ """
+ # TODO: Call openssl's EVP_BytesToKey instead of reimplementing by hand
+ # (through m2crypto?)
+ if salt is None:
+ salt = '\x00'*(key_size/2)
+ ks = key + salt
+ d = ['']
+ result = bytes()
+ i = 1
+ while len(result) < 2*key_size:
+ tohash = d[i-1] + ks
+ # man page for BytesTo
+ for hash_application in range(count):
+ tohash = hash_algo.new(tohash).digest()
+ d.append(tohash)
+ result = ''.join(d)
+ i += 1
+ return result[:-key_size], result[-key_size:]
+
+
def a_decrypt_item(data, key, aes_size=A_AES_SIZE):
key_size = KEY_SIZE[aes_size]
if data[:len(SALT_MARKER)] == SALT_MARKER:
@@ -83,22 +110,29 @@ def a_decrypt_item(data, key, aes_size=A_AES_SIZE):
kdf_rounds = KDF_ROUNDS_BY_SIZE[aes_size]
pb_gen = pbkdf1.PBKDF1(key, salt, rounds=kdf_rounds)
else:
- nkey = hashlib.md5(key).digest()
+ nkey = Crypto.Hash.MD5.new(key).digest()
iv = '\x00'*key_size
aes_er = Crypto.Cipher.AES.new(nkey, Crypto.Cipher.AES.MODE_CBC, iv)
return padding.pkcs5_unpad(aes_er.decrypt(data))
-def opdata1_decrypt_item(data, key, hmac_key, aes_size=C_AES_SIZE):
- key_size = KEY_SIZE[aes_size]
- assert len(key) == key_size
- assert len(data) >= OPDATA1_MINIMUM_SIZE
- assert data[:8] == "opdata1", "expected opdata1 format message"
+def opdata1_unpack(data):
+ if data[:8] != "opdata01":
+ data = base64.b64decode(data)
+ assert data[:8] == "opdata01", "expected opdata1 format message"
data = data[8:]
plaintext_length = struct.unpack("<Q", data[:8])
iv = data[8:24]
cryptext = data[24:-32]
expected_hmac = data[-32:]
+ return plaintext_length, iv, cryptext, expected_hmac
+
+
+def opdata1_decrypt_item(data, key, hmac_key, aes_size=C_AES_SIZE):
+ key_size = KEY_SIZE[aes_size]
+ assert len(key) == key_size
+ assert len(data) >= OPDATA1_MINIMUM_SIZE
+ plaintext_length, iv, cryptext, expected_hmac = opdata1_unpack(data)
verifier = hmac.new(key=hmac_key, digestmod=hashlib.sha256)
# TODO: put in "opdata1" and plantext_length or not?
verifier.update(iv)
@@ -107,3 +141,10 @@ def opdata1_decrypt_item(data, key, hmac_key, aes_size=C_AES_SIZE):
raise ValueError("HMAC did not match for opdata1 record")
decryptor = Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_CBC, iv)
return padding.ab_unpad(decryptor.decrypt(data), plaintext_length)
+
+def opdata1_derive_keys(password, salt, iterations=1000, aes_size=C_AES_SIZE):
+ key_size = KEY_SIZE[aes_size]
+ p_gen = pbkdf2.PBKDF2(passphrase=password, salt=salt, digestmodule=Crypto.Hash.SHA512, iterations=iterations)
+ key1 = p_gen.read(key_size)
+ key2 = p_gen.read(key_size)
+ return key1, key2
@@ -12,25 +12,44 @@
class AbstractKeychain(object):
"""Implementation of common keychain logic (MP design, etc)."""
- def check_version(self):
- 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)
-
-class AKeychain(AbstractKeychain):
- """Implementation of the classic .agilekeychain storage format"""
def __init__(self, path):
self._open(path)
def _open(self, path):
self.base_path = path
- if not(os.path.exists(self.base_path)):
- raise ValueError("Proported 1Password key file %s does not exist" % self.base_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)
@@ -65,4 +84,27 @@ class CKeychain(AbstractKeychain):
Documentation at http://learn.agilebits.com/1Password4/Security/keychain-design.html
"""
- pass
+
+ 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)
+
@@ -2,9 +2,14 @@
import testify as T
+import onepassword.keychain
class CloudKeychainIntegrationTestCase(T.TestCase):
- test_file_root = os.path.realpath(os.path.join(__file__, '..', '..', 'data', 'sample_cloudkeychain'))
+ test_file_root = os.path.realpath(os.path.join(__file__, '..', '..', '..', 'data', 'sample_cloudkeychain'))
+
+ def test_open(self):
+ c = onepassword.keychain.CKeychain(self.test_file_root)
+ c.unlock("fred")
if __name__ == '__main__':
No changes.
File renamed without changes.
File renamed without changes.

0 comments on commit d246ba6

Please sign in to comment.