key.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. from binascii import hexlify, unhexlify, a2b_base64, b2a_base64
  2. from getpass import getpass
  3. import os
  4. import msgpack
  5. import re
  6. import shutil
  7. import tempfile
  8. import textwrap
  9. import unittest
  10. import hmac
  11. from hashlib import sha256
  12. import zlib
  13. from .crypto import pbkdf2_sha256, get_random_bytes, AES, bytes_to_long, long_to_bytes, bytes_to_int
  14. from .helpers import IntegrityError, get_keys_dir, Location
  15. PREFIX = b'\0' * 8
  16. KEYFILE = b'\0'
  17. PASSPHRASE = b'\1'
  18. PLAINTEXT = b'\2'
  19. class HMAC(hmac.HMAC):
  20. def update(self, msg):
  21. self.inner.update(msg)
  22. def key_creator(repository, args):
  23. if args.keyfile:
  24. return KeyfileKey.create(repository, args)
  25. elif args.passphrase:
  26. return PassphraseKey.create(repository, args)
  27. else:
  28. return PlaintextKey.create(repository, args)
  29. def key_factory(repository, manifest_data):
  30. if manifest_data[:1] == KEYFILE:
  31. return KeyfileKey.detect(repository, manifest_data)
  32. elif manifest_data[:1] == PASSPHRASE:
  33. return PassphraseKey.detect(repository, manifest_data)
  34. elif manifest_data[:1] == PLAINTEXT:
  35. return PlaintextKey.detect(repository, manifest_data)
  36. else:
  37. raise Exception('Unkown Key type %d' % ord(manifest_data[0]))
  38. class KeyBase(object):
  39. def id_hash(self, data):
  40. """Return HMAC hash using the "id" HMAC key
  41. """
  42. def encrypt(self, data):
  43. pass
  44. def decrypt(self, id, data):
  45. pass
  46. class PlaintextKey(KeyBase):
  47. TYPE = PLAINTEXT
  48. chunk_seed = 0
  49. @classmethod
  50. def create(cls, repository, args):
  51. print('Encryption NOT enabled.\nUse the --key-file or --passphrase options to enable encryption.')
  52. return cls()
  53. @classmethod
  54. def detect(cls, repository, manifest_data):
  55. return cls()
  56. def id_hash(self, data):
  57. return sha256(data).digest()
  58. def encrypt(self, data):
  59. return b''.join([self.TYPE, zlib.compress(data)])
  60. def decrypt(self, id, data):
  61. if data[:1] != self.TYPE:
  62. raise IntegrityError('Invalid encryption envelope')
  63. data = zlib.decompress(memoryview(data)[1:])
  64. if id and sha256(data).digest() != id:
  65. raise IntegrityError('Chunk id verification failed')
  66. return data
  67. class AESKeyBase(KeyBase):
  68. def id_hash(self, data):
  69. """Return HMAC hash using the "id" HMAC key
  70. """
  71. return HMAC(self.id_key, data, sha256).digest()
  72. def encrypt(self, data):
  73. data = zlib.compress(data)
  74. self.enc_cipher.reset()
  75. data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(data)))
  76. hash = HMAC(self.enc_hmac_key, data, sha256).digest()
  77. return b''.join((self.TYPE, hash, data))
  78. def decrypt(self, id, data):
  79. if data[:1] != self.TYPE:
  80. raise IntegrityError('Invalid encryption envelope')
  81. hash = memoryview(data)[1:33]
  82. if memoryview(HMAC(self.enc_hmac_key, memoryview(data)[33:], sha256).digest()) != hash:
  83. raise IntegrityError('Encryption envelope checksum mismatch')
  84. self.dec_cipher.reset(iv=PREFIX + data[33:41])
  85. data = zlib.decompress(self.dec_cipher.decrypt(data[41:])) # should use memoryview
  86. if id and HMAC(self.id_key, data, sha256).digest() != id:
  87. raise IntegrityError('Chunk id verification failed')
  88. return data
  89. def extract_iv(self, payload):
  90. if payload[:1] != self.TYPE:
  91. raise IntegrityError('Invalid encryption envelope')
  92. nonce = bytes_to_long(payload[33:41])
  93. return nonce
  94. def init_from_random_data(self, data):
  95. self.enc_key = data[0:32]
  96. self.enc_hmac_key = data[32:64]
  97. self.id_key = data[64:96]
  98. self.chunk_seed = bytes_to_int(data[96:100])
  99. # Convert to signed int32
  100. if self.chunk_seed & 0x80000000:
  101. self.chunk_seed = self.chunk_seed - 0xffffffff - 1
  102. def init_ciphers(self, enc_iv=b''):
  103. self.enc_cipher = AES(self.enc_key, enc_iv)
  104. self.dec_cipher = AES(self.enc_key)
  105. class PassphraseKey(AESKeyBase):
  106. TYPE = PASSPHRASE
  107. iterations = 100000
  108. @classmethod
  109. def create(cls, repository, args):
  110. key = cls()
  111. passphrase = os.environ.get('DARC_PASSPHRASE')
  112. if passphrase is not None:
  113. passphrase2 = passphrase
  114. else:
  115. passphrase, passphrase2 = 1, 2
  116. while passphrase != passphrase2:
  117. passphrase = getpass('Enter passphrase: ')
  118. if not passphrase:
  119. print('Passphrase must not be blank')
  120. continue
  121. passphrase2 = getpass('Enter same passphrase again: ')
  122. if passphrase != passphrase2:
  123. print('Passphrases do not match')
  124. key.init(repository, passphrase)
  125. if passphrase:
  126. print('Remember your passphrase. Your data will be inaccessible without it.')
  127. return key
  128. @classmethod
  129. def detect(cls, repository, manifest_data):
  130. prompt = 'Enter passphrase for %s: ' % repository._location.orig
  131. key = cls()
  132. passphrase = os.environ.get('DARC_PASSPHRASE')
  133. if passphrase is None:
  134. passphrase = getpass(prompt)
  135. while True:
  136. key.init(repository, passphrase)
  137. try:
  138. key.decrypt(None, manifest_data)
  139. key.init_ciphers(PREFIX + long_to_bytes(key.extract_iv(manifest_data) + 1000))
  140. return key
  141. except IntegrityError:
  142. passphrase = getpass(prompt)
  143. def init(self, repository, passphrase):
  144. self.init_from_random_data(pbkdf2_sha256(passphrase.encode('utf-8'), repository.id, self.iterations, 100))
  145. self.init_ciphers()
  146. class KeyfileKey(AESKeyBase):
  147. FILE_ID = 'DARC KEY'
  148. TYPE = KEYFILE
  149. @classmethod
  150. def detect(cls, repository, manifest_data):
  151. key = cls()
  152. path = cls.find_key_file(repository)
  153. prompt = 'Enter passphrase for key file %s: ' % path
  154. passphrase = os.environ.get('DARC_PASSPHRASE', '')
  155. while not key.load(path, passphrase):
  156. passphrase = getpass(prompt)
  157. key.init_ciphers(PREFIX + long_to_bytes(key.extract_iv(manifest_data) + 1000))
  158. return key
  159. @classmethod
  160. def find_key_file(cls, repository):
  161. id = hexlify(repository.id).decode('ascii')
  162. keys_dir = get_keys_dir()
  163. for name in os.listdir(keys_dir):
  164. filename = os.path.join(keys_dir, name)
  165. with open(filename, 'r') as fd:
  166. line = fd.readline().strip()
  167. if line and line.startswith(cls.FILE_ID) and line[9:] == id:
  168. return filename
  169. raise Exception('Key file for repository with ID %s not found' % id)
  170. def load(self, filename, passphrase):
  171. with open(filename, 'r') as fd:
  172. cdata = a2b_base64(''.join(fd.readlines()[1:]).encode('ascii')) # .encode needed for Python 3.[0-2]
  173. data = self.decrypt_key_file(cdata, passphrase)
  174. if data:
  175. key = msgpack.unpackb(data)
  176. if key[b'version'] != 1:
  177. raise IntegrityError('Invalid key file header')
  178. self.repository_id = key[b'repository_id']
  179. self.enc_key = key[b'enc_key']
  180. self.enc_hmac_key = key[b'enc_hmac_key']
  181. self.id_key = key[b'id_key']
  182. self.chunk_seed = key[b'chunk_seed']
  183. self.path = filename
  184. return True
  185. def decrypt_key_file(self, data, passphrase):
  186. d = msgpack.unpackb(data)
  187. assert d[b'version'] == 1
  188. assert d[b'algorithm'] == b'sha256'
  189. key = pbkdf2_sha256(passphrase.encode('utf-8'), d[b'salt'], d[b'iterations'], 32)
  190. data = AES(key).decrypt(d[b'data'])
  191. if HMAC(key, data, sha256).digest() != d[b'hash']:
  192. return None
  193. return data
  194. def encrypt_key_file(self, data, passphrase):
  195. salt = get_random_bytes(32)
  196. iterations = 100000
  197. key = pbkdf2_sha256(passphrase.encode('utf-8'), salt, iterations, 32)
  198. hash = HMAC(key, data, sha256).digest()
  199. cdata = AES(key).encrypt(data)
  200. d = {
  201. 'version': 1,
  202. 'salt': salt,
  203. 'iterations': iterations,
  204. 'algorithm': 'sha256',
  205. 'hash': hash,
  206. 'data': cdata,
  207. }
  208. return msgpack.packb(d)
  209. def save(self, path, passphrase):
  210. key = {
  211. 'version': 1,
  212. 'repository_id': self.repository_id,
  213. 'enc_key': self.enc_key,
  214. 'enc_hmac_key': self.enc_hmac_key,
  215. 'id_key': self.id_key,
  216. 'chunk_seed': self.chunk_seed,
  217. }
  218. data = self.encrypt_key_file(msgpack.packb(key), passphrase)
  219. with open(path, 'w') as fd:
  220. fd.write('%s %s\n' % (self.FILE_ID, hexlify(self.repository_id).decode('ascii')))
  221. fd.write('\n'.join(textwrap.wrap(b2a_base64(data).decode('ascii'))))
  222. self.path = path
  223. def change_passphrase(self):
  224. passphrase, passphrase2 = 1, 2
  225. while passphrase != passphrase2:
  226. passphrase = getpass('New passphrase: ')
  227. passphrase2 = getpass('Enter same passphrase again: ')
  228. if passphrase != passphrase2:
  229. print('Passphrases do not match')
  230. self.save(self.path, passphrase)
  231. print('Key file "%s" updated' % self.path)
  232. @classmethod
  233. def create(cls, repository, args):
  234. filename = args.repository.to_key_filename()
  235. path = filename
  236. i = 1
  237. while os.path.exists(path):
  238. i += 1
  239. path = filename + '.%d' % i
  240. passphrase = os.environ.get('DARC_PASSPHRASE')
  241. if passphrase is not None:
  242. passphrase2 = passphrase
  243. else:
  244. passphrase, passphrase2 = 1, 2
  245. while passphrase != passphrase2:
  246. passphrase = getpass('Enter passphrase (empty for no passphrase):')
  247. passphrase2 = getpass('Enter same passphrase again: ')
  248. if passphrase != passphrase2:
  249. print('Passphrases do not match')
  250. key = cls()
  251. key.repository_id = repository.id
  252. key.init_from_random_data(get_random_bytes(100))
  253. key.init_ciphers()
  254. key.save(path, passphrase)
  255. print('Key file "%s" created.' % key.path)
  256. print('Keep this file safe. Your data will be inaccessible without it.')
  257. return key
  258. class KeyTestCase(unittest.TestCase):
  259. class MockArgs(object):
  260. repository = Location(tempfile.mkstemp()[1])
  261. keyfile2_key_file = """
  262. DARC KEY 0000000000000000000000000000000000000000000000000000000000000000
  263. hqppdGVyYXRpb25zzgABhqCkaGFzaNoAIMyonNI+7Cjv0qHi0AOBM6bLGxACJhfgzVD2oq
  264. bIS9SFqWFsZ29yaXRobaZzaGEyNTakc2FsdNoAINNK5qqJc1JWSUjACwFEWGTdM7Nd0a5l
  265. 1uBGPEb+9XM9p3ZlcnNpb24BpGRhdGHaANAYDT5yfPpU099oBJwMomsxouKyx/OG4QIXK2
  266. hQCG2L2L/9PUu4WIuKvGrsXoP7syemujNfcZws5jLp2UPva4PkQhQsrF1RYDEMLh2eF9Ol
  267. rwtkThq1tnh7KjWMG9Ijt7/aoQtq0zDYP/xaFF8XXSJxiyP5zjH5+spB6RL0oQHvbsliSh
  268. /cXJq7jrqmrJ1phd6dg4SHAM/i+hubadZoS6m25OQzYAW09wZD/phG8OVa698Z5ed3HTaT
  269. SmrtgJL3EoOKgUI9d6BLE4dJdBqntifo""".strip()
  270. keyfile2_cdata = unhexlify(re.sub('\W', '', """
  271. 0055f161493fcfc16276e8c31493c4641e1eb19a79d0326fad0291e5a9c98e5933
  272. 00000000000003e8d21eaf9b86c297a8cd56432e1915bb
  273. """))
  274. keyfile2_id = unhexlify('c3fbf14bc001ebcc3cd86e696c13482ed071740927cd7cbe1b01b4bfcee49314')
  275. def setUp(self):
  276. self.tmppath = tempfile.mkdtemp()
  277. os.environ['DARC_KEYS_DIR'] = self.tmppath
  278. def tearDown(self):
  279. shutil.rmtree(self.tmppath)
  280. class MockRepository(object):
  281. class _Location(object):
  282. orig = '/some/place'
  283. _location = _Location()
  284. id = bytes(32)
  285. def setUp(self):
  286. self.tmpdir = tempfile.mkdtemp()
  287. os.environ['DARC_KEYS_DIR'] = self.tmpdir
  288. def tearDown(self):
  289. shutil.rmtree(self.tmpdir)
  290. def test_plaintext(self):
  291. key = PlaintextKey.create(None, None)
  292. data = b'foo'
  293. self.assertEqual(hexlify(key.id_hash(data)), b'2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae')
  294. self.assertEqual(data, key.decrypt(key.id_hash(data), key.encrypt(data)))
  295. def test_keyfile(self):
  296. os.environ['DARC_PASSPHRASE'] = 'test'
  297. key = KeyfileKey.create(self.MockRepository(), self.MockArgs())
  298. self.assertEqual(bytes_to_long(key.enc_cipher.iv, 8), 0)
  299. manifest = key.encrypt(b'')
  300. iv = key.extract_iv(manifest)
  301. key2 = KeyfileKey.detect(self.MockRepository(), manifest)
  302. self.assertEqual(bytes_to_long(key2.enc_cipher.iv, 8), iv + 1000)
  303. # Key data sanity check
  304. self.assertEqual(len(set([key2.id_key, key2.enc_key, key2.enc_hmac_key])), 3)
  305. self.assertEqual(key2.chunk_seed == 0, False)
  306. data = b'foo'
  307. self.assertEqual(data, key2.decrypt(key.id_hash(data), key.encrypt(data)))
  308. def test_keyfile2(self):
  309. with open(os.path.join(os.environ['DARC_KEYS_DIR'], 'keyfile'), 'w') as fd:
  310. fd.write(self.keyfile2_key_file)
  311. os.environ['DARC_PASSPHRASE'] = 'passphrase'
  312. key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)
  313. self.assertEqual(key.decrypt(self.keyfile2_id, self.keyfile2_cdata), b'payload')
  314. def test_passphrase(self):
  315. os.environ['DARC_PASSPHRASE'] = 'test'
  316. key = PassphraseKey.create(self.MockRepository(), None)
  317. self.assertEqual(bytes_to_long(key.enc_cipher.iv, 8), 0)
  318. self.assertEqual(hexlify(key.id_key), b'793b0717f9d8fb01c751a487e9b827897ceea62409870600013fbc6b4d8d7ca6')
  319. self.assertEqual(hexlify(key.enc_hmac_key), b'b885a05d329a086627412a6142aaeb9f6c54ab7950f996dd65587251f6bc0901')
  320. self.assertEqual(hexlify(key.enc_key), b'2ff3654c6daf7381dbbe718d2b20b4f1ea1e34caa6cc65f6bb3ac376b93fed2a')
  321. self.assertEqual(key.chunk_seed, -775740477)
  322. manifest = key.encrypt(b'')
  323. iv = key.extract_iv(manifest)
  324. key2 = PassphraseKey.detect(self.MockRepository(), manifest)
  325. self.assertEqual(bytes_to_long(key2.enc_cipher.iv, 8), iv + 1000)
  326. self.assertEqual(key.id_key, key2.id_key)
  327. self.assertEqual(key.enc_hmac_key, key2.enc_hmac_key)
  328. self.assertEqual(key.enc_key, key2.enc_key)
  329. self.assertEqual(key.chunk_seed, key2.chunk_seed)
  330. data = b'foo'
  331. self.assertEqual(hexlify(key.id_hash(data)), b'818217cf07d37efad3860766dcdf1d21e401650fed2d76ed1d797d3aae925990')
  332. self.assertEqual(data, key2.decrypt(key2.id_hash(data), key.encrypt(data)))
  333. def suite():
  334. return unittest.TestLoader().loadTestsFromTestCase(KeyTestCase)
  335. if __name__ == '__main__':
  336. unittest.main()