key.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. from binascii import hexlify, a2b_base64, b2a_base64
  2. from getpass import getpass
  3. import os
  4. import msgpack
  5. import textwrap
  6. import hmac
  7. from hashlib import sha256
  8. import zlib
  9. from .crypto import pbkdf2_sha256, get_random_bytes, AES, bytes_to_long, long_to_bytes, bytes_to_int
  10. from .helpers import IntegrityError, get_keys_dir
  11. PREFIX = b'\0' * 8
  12. class HMAC(hmac.HMAC):
  13. def update(self, msg):
  14. self.inner.update(msg)
  15. def key_creator(repository, args):
  16. if args.keyfile:
  17. return KeyfileKey.create(repository, args)
  18. elif args.passphrase:
  19. return PassphraseKey.create(repository, args)
  20. else:
  21. return PlaintextKey.create(repository, args)
  22. def key_factory(repository, manifest_data):
  23. if manifest_data[0] == KeyfileKey.TYPE:
  24. return KeyfileKey.detect(repository, manifest_data)
  25. elif manifest_data[0] == PassphraseKey.TYPE:
  26. return PassphraseKey.detect(repository, manifest_data)
  27. elif manifest_data[0] == PlaintextKey.TYPE:
  28. return PlaintextKey.detect(repository, manifest_data)
  29. else:
  30. raise Exception('Unkown Key type %d' % ord(manifest_data[0]))
  31. class KeyBase(object):
  32. def __init__(self):
  33. self.TYPE_STR = bytes([self.TYPE])
  34. def id_hash(self, data):
  35. """Return HMAC hash using the "id" HMAC key
  36. """
  37. def encrypt(self, data):
  38. pass
  39. def decrypt(self, id, data):
  40. pass
  41. class PlaintextKey(KeyBase):
  42. TYPE = 0x02
  43. chunk_seed = 0
  44. @classmethod
  45. def create(cls, repository, args):
  46. print('Encryption NOT enabled.\nUse the --key-file or --passphrase options to enable encryption.')
  47. return cls()
  48. @classmethod
  49. def detect(cls, repository, manifest_data):
  50. return cls()
  51. def id_hash(self, data):
  52. return sha256(data).digest()
  53. def encrypt(self, data):
  54. return b''.join([self.TYPE_STR, zlib.compress(data)])
  55. def decrypt(self, id, data):
  56. if data[0] != self.TYPE:
  57. raise IntegrityError('Invalid encryption envelope')
  58. data = zlib.decompress(memoryview(data)[1:])
  59. if id and sha256(data).digest() != id:
  60. raise IntegrityError('Chunk id verification failed')
  61. return data
  62. class AESKeyBase(KeyBase):
  63. def id_hash(self, data):
  64. """Return HMAC hash using the "id" HMAC key
  65. """
  66. return HMAC(self.id_key, data, sha256).digest()
  67. def encrypt(self, data):
  68. data = zlib.compress(data)
  69. self.enc_cipher.reset()
  70. data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(data)))
  71. hmac = HMAC(self.enc_hmac_key, data, sha256).digest()
  72. return b''.join((self.TYPE_STR, hmac, data))
  73. def decrypt(self, id, data):
  74. if data[0] != self.TYPE:
  75. raise IntegrityError('Invalid encryption envelope')
  76. hmac = memoryview(data)[1:33]
  77. if memoryview(HMAC(self.enc_hmac_key, memoryview(data)[33:], sha256).digest()) != hmac:
  78. raise IntegrityError('Encryption envelope checksum mismatch')
  79. self.dec_cipher.reset(iv=PREFIX + data[33:41])
  80. data = zlib.decompress(self.dec_cipher.decrypt(data[41:])) # should use memoryview
  81. if id and HMAC(self.id_key, data, sha256).digest() != id:
  82. raise IntegrityError('Chunk id verification failed')
  83. return data
  84. def extract_iv(self, payload):
  85. if payload[0] != self.TYPE:
  86. raise IntegrityError('Invalid encryption envelope')
  87. nonce = bytes_to_long(payload[33:41])
  88. return nonce
  89. def init_from_random_data(self, data):
  90. self.enc_key = data[0:32]
  91. self.enc_hmac_key = data[32:64]
  92. self.id_key = data[64:96]
  93. self.chunk_seed = bytes_to_int(data[96:100])
  94. # Convert to signed int32
  95. if self.chunk_seed & 0x80000000:
  96. self.chunk_seed = self.chunk_seed - 0xffffffff - 1
  97. def init_ciphers(self, enc_iv=b''):
  98. self.enc_cipher = AES(self.enc_key, enc_iv)
  99. self.dec_cipher = AES(self.enc_key)
  100. class PassphraseKey(AESKeyBase):
  101. TYPE = 0x01
  102. iterations = 100000
  103. @classmethod
  104. def create(cls, repository, args):
  105. key = cls()
  106. passphrase = os.environ.get('DARC_PASSPHRASE')
  107. if passphrase is not None:
  108. passphrase2 = passphrase
  109. else:
  110. passphrase, passphrase2 = 1, 2
  111. while passphrase != passphrase2:
  112. passphrase = getpass('Enter passphrase: ')
  113. if not passphrase:
  114. print('Passphrase must not be blank')
  115. continue
  116. passphrase2 = getpass('Enter same passphrase again: ')
  117. if passphrase != passphrase2:
  118. print('Passphrases do not match')
  119. key.init(repository, passphrase)
  120. if passphrase:
  121. print('Remember your passphrase. Your data will be inaccessible without it.')
  122. return key
  123. @classmethod
  124. def detect(cls, repository, manifest_data):
  125. prompt = 'Enter passphrase for %s: ' % repository._location.orig
  126. key = cls()
  127. passphrase = os.environ.get('DARC_PASSPHRASE')
  128. if passphrase is None:
  129. passphrase = getpass(prompt)
  130. while True:
  131. key.init(repository, passphrase)
  132. try:
  133. key.decrypt(None, manifest_data)
  134. key.init_ciphers(PREFIX + long_to_bytes(key.extract_iv(manifest_data) + 1000))
  135. return key
  136. except IntegrityError:
  137. passphrase = getpass(prompt)
  138. def init(self, repository, passphrase):
  139. self.init_from_random_data(pbkdf2_sha256(passphrase.encode('utf-8'), repository.id, self.iterations, 100))
  140. self.init_ciphers()
  141. class KeyfileKey(AESKeyBase):
  142. FILE_ID = 'DARC KEY'
  143. TYPE = 0x00
  144. @classmethod
  145. def detect(cls, repository, manifest_data):
  146. key = cls()
  147. path = cls.find_key_file(repository)
  148. prompt = 'Enter passphrase for key file %s: ' % path
  149. passphrase = os.environ.get('DARC_PASSPHRASE', '')
  150. while not key.load(path, passphrase):
  151. passphrase = getpass(prompt)
  152. key.init_ciphers(PREFIX + long_to_bytes(key.extract_iv(manifest_data) + 1000))
  153. return key
  154. @classmethod
  155. def find_key_file(cls, repository):
  156. id = hexlify(repository.id).decode('ascii')
  157. keys_dir = get_keys_dir()
  158. for name in os.listdir(keys_dir):
  159. filename = os.path.join(keys_dir, name)
  160. with open(filename, 'r') as fd:
  161. line = fd.readline().strip()
  162. if line and line.startswith(cls.FILE_ID) and line[9:] == id:
  163. return filename
  164. raise Exception('Key file for repository with ID %s not found' % id)
  165. def load(self, filename, passphrase):
  166. with open(filename, 'r') as fd:
  167. cdata = a2b_base64(''.join(fd.readlines()[1:]).encode('ascii')) # .encode needed for Python 3.[0-2]
  168. data = self.decrypt_key_file(cdata, passphrase)
  169. if data:
  170. key = msgpack.unpackb(data)
  171. if key[b'version'] != 1:
  172. raise IntegrityError('Invalid key file header')
  173. self.repository_id = key[b'repository_id']
  174. self.enc_key = key[b'enc_key']
  175. self.enc_hmac_key = key[b'enc_hmac_key']
  176. self.id_key = key[b'id_key']
  177. self.chunk_seed = key[b'chunk_seed']
  178. self.path = filename
  179. return True
  180. def decrypt_key_file(self, data, passphrase):
  181. d = msgpack.unpackb(data)
  182. assert d[b'version'] == 1
  183. assert d[b'algorithm'] == b'sha256'
  184. key = pbkdf2_sha256(passphrase.encode('utf-8'), d[b'salt'], d[b'iterations'], 32)
  185. data = AES(key).decrypt(d[b'data'])
  186. if HMAC(key, data, sha256).digest() != d[b'hash']:
  187. return None
  188. return data
  189. def encrypt_key_file(self, data, passphrase):
  190. salt = get_random_bytes(32)
  191. iterations = 100000
  192. key = pbkdf2_sha256(passphrase.encode('utf-8'), salt, iterations, 32)
  193. hash = HMAC(key, data, sha256).digest()
  194. cdata = AES(key).encrypt(data)
  195. d = {
  196. 'version': 1,
  197. 'salt': salt,
  198. 'iterations': iterations,
  199. 'algorithm': 'sha256',
  200. 'hash': hash,
  201. 'data': cdata,
  202. }
  203. return msgpack.packb(d)
  204. def save(self, path, passphrase):
  205. key = {
  206. 'version': 1,
  207. 'repository_id': self.repository_id,
  208. 'enc_key': self.enc_key,
  209. 'enc_hmac_key': self.enc_hmac_key,
  210. 'id_key': self.id_key,
  211. 'chunk_seed': self.chunk_seed,
  212. }
  213. data = self.encrypt_key_file(msgpack.packb(key), passphrase)
  214. with open(path, 'w') as fd:
  215. fd.write('%s %s\n' % (self.FILE_ID, hexlify(self.repository_id).decode('ascii')))
  216. fd.write('\n'.join(textwrap.wrap(b2a_base64(data).decode('ascii'))))
  217. self.path = path
  218. def change_passphrase(self):
  219. passphrase, passphrase2 = 1, 2
  220. while passphrase != passphrase2:
  221. passphrase = getpass('New passphrase: ')
  222. passphrase2 = getpass('Enter same passphrase again: ')
  223. if passphrase != passphrase2:
  224. print('Passphrases do not match')
  225. self.save(self.path, passphrase)
  226. print('Key file "%s" updated' % self.path)
  227. @classmethod
  228. def create(cls, repository, args):
  229. filename = args.repository.to_key_filename()
  230. path = filename
  231. i = 1
  232. while os.path.exists(path):
  233. i += 1
  234. path = filename + '.%d' % i
  235. passphrase = os.environ.get('DARC_PASSPHRASE')
  236. if passphrase is not None:
  237. passphrase2 = passphrase
  238. else:
  239. passphrase, passphrase2 = 1, 2
  240. while passphrase != passphrase2:
  241. passphrase = getpass('Enter passphrase (empty for no passphrase):')
  242. passphrase2 = getpass('Enter same passphrase again: ')
  243. if passphrase != passphrase2:
  244. print('Passphrases do not match')
  245. key = cls()
  246. key.repository_id = repository.id
  247. key.init_from_random_data(get_random_bytes(100))
  248. key.init_ciphers()
  249. key.save(path, passphrase)
  250. print('Key file "%s" created.' % key.path)
  251. print('Keep this file safe. Your data will be inaccessible without it.')
  252. return key