key.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. from __future__ import with_statement
  2. from getpass import getpass
  3. import os
  4. import msgpack
  5. import tempfile
  6. import unittest
  7. import zlib
  8. from Crypto.Cipher import AES
  9. from Crypto.Hash import SHA256, HMAC
  10. from Crypto.Util import Counter
  11. from Crypto.Util.number import bytes_to_long, long_to_bytes
  12. from Crypto.Random import get_random_bytes
  13. from Crypto.Protocol.KDF import PBKDF2
  14. from .helpers import IntegrityError, get_keys_dir
  15. PREFIX = '\0' * 8
  16. KEYFILE = '\0'
  17. PASSPHRASE = '\1'
  18. PLAINTEXT = '\2'
  19. def key_creator(store, args):
  20. if args.keyfile:
  21. return KeyfileKey.create(store, args)
  22. elif args.passphrase:
  23. return PassphraseKey.create(store, args)
  24. else:
  25. return PlaintextKey.create(store, args)
  26. def key_factory(store, manifest_data):
  27. if manifest_data[0] == KEYFILE:
  28. return KeyfileKey.detect(store, manifest_data)
  29. elif manifest_data[0] == PASSPHRASE:
  30. return PassphraseKey.detect(store, manifest_data)
  31. elif manifest_data[0] == PLAINTEXT:
  32. return PlaintextKey.detect(store, manifest_data)
  33. else:
  34. raise Exception('Unkown Key type %d' % ord(manifest_data[0]))
  35. def SHA256_PDF(p, s):
  36. return HMAC.new(p, s, SHA256).digest()
  37. class KeyBase(object):
  38. def id_hash(self, data):
  39. """Return HMAC hash using the "id" HMAC key
  40. """
  41. def encrypt(self, data):
  42. pass
  43. def decrypt(self, id, data):
  44. pass
  45. def post_manifest_load(self, config):
  46. pass
  47. def pre_manifest_write(self, manifest):
  48. pass
  49. class PlaintextKey(KeyBase):
  50. TYPE = PLAINTEXT
  51. chunk_seed = 0
  52. @classmethod
  53. def create(cls, store, args):
  54. print 'Encryption NOT enabled.\nUse the --key-file or --passphrase options to enable encryption.'
  55. return cls()
  56. @classmethod
  57. def detect(cls, store, manifest_data):
  58. return cls()
  59. def id_hash(self, data):
  60. return SHA256.new(data).digest()
  61. def encrypt(self, data):
  62. return ''.join([self.TYPE, zlib.compress(data)])
  63. def decrypt(self, id, data):
  64. if data[0] != self.TYPE:
  65. raise IntegrityError('Invalid encryption envelope')
  66. data = zlib.decompress(data[1:])
  67. if id and SHA256.new(data).digest() != id:
  68. raise IntegrityError('Chunk id verification failed')
  69. return data
  70. class AESKeyBase(KeyBase):
  71. def post_manifest_load(self, config):
  72. iv = bytes_to_long(config['aes_counter']) + 100
  73. self.counter = Counter.new(64, initial_value=iv, prefix=PREFIX)
  74. def pre_manifest_write(self, manifest):
  75. manifest.config['aes_counter'] = long_to_bytes(self.counter.next_value(), 8)
  76. def id_hash(self, data):
  77. """Return HMAC hash using the "id" HMAC key
  78. """
  79. return HMAC.new(self.id_key, data, SHA256).digest()
  80. def encrypt(self, data):
  81. data = zlib.compress(data)
  82. nonce = long_to_bytes(self.counter.next_value(), 8)
  83. data = ''.join((nonce, AES.new(self.enc_key, AES.MODE_CTR, '',
  84. counter=self.counter).encrypt(data)))
  85. hash = HMAC.new(self.enc_hmac_key, data, SHA256).digest()
  86. return ''.join((self.TYPE, hash, data))
  87. def decrypt(self, id, data):
  88. if data[0] != self.TYPE:
  89. raise IntegrityError('Invalid encryption envelope')
  90. hash = data[1:33]
  91. if HMAC.new(self.enc_hmac_key, data[33:], SHA256).digest() != hash:
  92. raise IntegrityError('Encryption envelope checksum mismatch')
  93. nonce = bytes_to_long(data[33:41])
  94. counter = Counter.new(64, initial_value=nonce, prefix=PREFIX)
  95. data = zlib.decompress(AES.new(self.enc_key, AES.MODE_CTR, counter=counter).decrypt(data[41:]))
  96. if id and HMAC.new(self.id_key, data, SHA256).digest() != id:
  97. raise IntegrityError('Chunk id verification failed')
  98. return data
  99. def init_from_random_data(self, data):
  100. self.enc_key = data[0:32]
  101. self.enc_hmac_key = data[32:64]
  102. self.id_key = data[64:96]
  103. self.chunk_seed = bytes_to_long(data[96:100])
  104. # Convert to signed int32
  105. if self.chunk_seed & 0x80000000:
  106. self.chunk_seed = self.chunk_seed - 0xffffffff - 1
  107. self.counter = Counter.new(64, initial_value=1, prefix=PREFIX)
  108. class PassphraseKey(AESKeyBase):
  109. TYPE = PASSPHRASE
  110. iterations = 10000
  111. @classmethod
  112. def create(cls, store, args):
  113. key = cls()
  114. passphrase = os.environ.get('DARC_PASSPHRASE')
  115. if passphrase is not None:
  116. passphrase2 = passphrase
  117. else:
  118. passphrase, passphrase2 = 1, 2
  119. while passphrase != passphrase2:
  120. passphrase = getpass('Enter passphrase: ')
  121. if not passphrase:
  122. print 'Passphrase must not be blank'
  123. continue
  124. passphrase2 = getpass('Enter same passphrase again: ')
  125. if passphrase != passphrase2:
  126. print 'Passphrases do not match'
  127. key.init(store, passphrase)
  128. if passphrase:
  129. print 'Remember your passphrase. Your data will be inaccessible without it.'
  130. return key
  131. @classmethod
  132. def detect(cls, store, manifest_data):
  133. prompt = 'Enter passphrase for %s: ' % store._location.orig
  134. key = cls()
  135. passphrase = os.environ.get('DARC_PASSPHRASE')
  136. if passphrase is None:
  137. passphrase = getpass(prompt)
  138. while True:
  139. key.init(store, passphrase)
  140. try:
  141. key.decrypt(None, manifest_data)
  142. return key
  143. except IntegrityError:
  144. passphrase = getpass(prompt)
  145. def init(self, store, passphrase):
  146. self.init_from_random_data(PBKDF2(passphrase, store.id, 100, self.iterations, SHA256_PDF))
  147. class KeyfileKey(AESKeyBase):
  148. FILE_ID = 'DARC KEY'
  149. TYPE = KEYFILE
  150. @classmethod
  151. def detect(cls, store, manifest_data):
  152. key = cls()
  153. path = cls.find_key_file(store)
  154. prompt = 'Enter passphrase for key file %s: ' % path
  155. passphrase = os.environ.get('DARC_PASSPHRASE', '')
  156. while not key.load(path, passphrase):
  157. passphrase = getpass(prompt)
  158. return key
  159. @classmethod
  160. def find_key_file(cls, store):
  161. id = store.id.encode('hex')
  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, 'rb') 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 store with ID %s not found' % id)
  170. def load(self, filename, passphrase):
  171. with open(filename, 'rb') as fd:
  172. cdata = (''.join(fd.readlines()[1:])).decode('base64')
  173. data = self.decrypt_key_file(cdata, passphrase)
  174. if data:
  175. key = msgpack.unpackb(data)
  176. if key['version'] != 1:
  177. raise IntegrityError('Invalid key file header')
  178. self.store_id = key['store_id']
  179. self.enc_key = key['enc_key']
  180. self.enc_hmac_key = key['enc_hmac_key']
  181. self.id_key = key['id_key']
  182. self.chunk_seed = key['chunk_seed']
  183. self.counter = Counter.new(64, initial_value=1, prefix=PREFIX)
  184. self.path = filename
  185. return True
  186. def decrypt_key_file(self, data, passphrase):
  187. d = msgpack.unpackb(data)
  188. assert d['version'] == 1
  189. assert d['algorithm'] == 'SHA256'
  190. key = PBKDF2(passphrase, d['salt'], 32, d['iterations'], SHA256_PDF)
  191. data = AES.new(key, AES.MODE_CTR, counter=Counter.new(128)).decrypt(d['data'])
  192. if HMAC.new(key, data, SHA256).digest() != d['hash']:
  193. return None
  194. return data
  195. def encrypt_key_file(self, data, passphrase):
  196. salt = get_random_bytes(32)
  197. iterations = 10000
  198. key = PBKDF2(passphrase, salt, 32, iterations, SHA256_PDF)
  199. hash = HMAC.new(key, data, SHA256).digest()
  200. cdata = AES.new(key, AES.MODE_CTR, counter=Counter.new(128)).encrypt(data)
  201. d = {
  202. 'version': 1,
  203. 'salt': salt,
  204. 'iterations': iterations,
  205. 'algorithm': 'SHA256',
  206. 'hash': hash,
  207. 'data': cdata,
  208. }
  209. return msgpack.packb(d)
  210. def save(self, path, passphrase):
  211. key = {
  212. 'version': 1,
  213. 'store_id': self.store_id,
  214. 'enc_key': self.enc_key,
  215. 'enc_hmac_key': self.enc_hmac_key,
  216. 'id_key': self.enc_key,
  217. 'chunk_seed': self.chunk_seed,
  218. }
  219. data = self.encrypt_key_file(msgpack.packb(key), passphrase)
  220. with open(path, 'wb') as fd:
  221. fd.write('%s %s\n' % (self.FILE_ID, self.store_id.encode('hex')))
  222. fd.write(data.encode('base64'))
  223. self.path = path
  224. def change_passphrase(self):
  225. passphrase, passphrase2 = 1, 2
  226. while passphrase != passphrase2:
  227. passphrase = getpass('New passphrase: ')
  228. passphrase2 = getpass('Enter same passphrase again: ')
  229. if passphrase != passphrase2:
  230. print 'Passphrases do not match'
  231. self.save(self.path, passphrase)
  232. print 'Key file "%s" updated' % self.path
  233. @classmethod
  234. def create(cls, store, args):
  235. filename = args.store.to_key_filename()
  236. path = filename
  237. i = 1
  238. while os.path.exists(path):
  239. i += 1
  240. path = filename + '.%d' % i
  241. passphrase = os.environ.get('DARC_PASSPHRASE')
  242. if passphrase is not None:
  243. passphrase2 = passphrase
  244. else:
  245. passphrase, passphrase2 = 1, 2
  246. while passphrase != passphrase2:
  247. passphrase = getpass('Enter passphrase (empty for no passphrase):')
  248. passphrase2 = getpass('Enter same passphrase again: ')
  249. if passphrase != passphrase2:
  250. print 'Passphrases do not match'
  251. key = cls()
  252. key.store_id = store.id
  253. key.init_from_random_data(get_random_bytes(100))
  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 MockStore(object):
  260. id = '\0' * 32
  261. def test_plaintext(self):
  262. key = PlaintextKey.create(None, None)
  263. data = 'foo'
  264. self.assertEqual(key.id_hash(data).encode('hex'), '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae')
  265. self.assertEqual(data, key.decrypt(key.id_hash(data), key.encrypt(data)))
  266. def test_keyfile(self):
  267. class MockArgs(object):
  268. class StoreArg(object):
  269. def to_key_filename(self):
  270. return tempfile.mkstemp()[1]
  271. store = StoreArg()
  272. os.environ['DARC_PASSPHRASE'] = 'test'
  273. key = KeyfileKey.create(self.MockStore(), MockArgs())
  274. data = 'foo'
  275. self.assertEqual(data, key.decrypt(key.id_hash(data), key.encrypt(data)))
  276. def test_passphrase(self):
  277. os.environ['DARC_PASSPHRASE'] = 'test'
  278. key = PassphraseKey.create(self.MockStore(), None)
  279. self.assertEqual(key.id_key.encode('hex'), 'f28e915da78a972786da47fee6c4bd2960a421b9bdbdb35a7942eb82552e9a72')
  280. self.assertEqual(key.enc_hmac_key.encode('hex'), '169c6082f209e524ea97e2c75318936f6e93c101b9345942a95491e9ae1738ca')
  281. self.assertEqual(key.enc_key.encode('hex'), 'c05dd423843d4dd32a52e4dc07bb11acabe215917fc5cf3a3df6c92b47af79ba')
  282. self.assertEqual(key.chunk_seed, -324662077)
  283. data = 'foo'
  284. self.assertEqual(key.id_hash(data).encode('hex'), '016c27cd40dc8e84f196f3b43a9424e8472897e09f6935d0d3a82fb41664bad7')
  285. self.assertEqual(data, key.decrypt(key.id_hash(data), key.encrypt(data)))
  286. def suite():
  287. return unittest.TestLoader().loadTestsFromTestCase(KeyTestCase)
  288. if __name__ == '__main__':
  289. unittest.main()