key.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. from binascii import hexlify, a2b_base64, b2a_base64
  2. from getpass import getpass
  3. import os
  4. import msgpack
  5. import textwrap
  6. from collections import namedtuple
  7. import hmac
  8. from hashlib import sha256, sha512
  9. import zlib
  10. try:
  11. import lzma # python >= 3.3
  12. except ImportError:
  13. try:
  14. from backports import lzma # backports.lzma from pypi
  15. except ImportError:
  16. lzma = None
  17. from attic.crypto import pbkdf2_sha256, get_random_bytes, AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks
  18. from attic.helpers import IntegrityError, get_keys_dir, Error
  19. PREFIX = b'\0' * 8
  20. class UnsupportedPayloadError(Error):
  21. """Unsupported payload type {}. A newer version is required to access this repository.
  22. """
  23. class sha512_256(object): # note: can't subclass sha512
  24. """sha512, but digest truncated to 256bit - faster than sha256 on 64bit platforms"""
  25. digestsize = digest_size = 32
  26. block_size = 64
  27. def __init__(self, data=None):
  28. self.name = 'sha512-256'
  29. self._h = sha512()
  30. if data:
  31. self.update(data)
  32. def update(self, data):
  33. self._h.update(data)
  34. def digest(self):
  35. return self._h.digest()[:self.digest_size]
  36. def hexdigest(self):
  37. return self._h.hexdigest()[:self.digest_size * 2]
  38. def copy(self):
  39. new = sha512_256.__new__(sha512_256)
  40. new._h = self._h.copy()
  41. return new
  42. class HMAC(hmac.HMAC):
  43. """Workaround a bug in Python < 3.4 Where HMAC does not accept memoryviews
  44. """
  45. def update(self, msg):
  46. self.inner.update(msg)
  47. class SHA256(object): # note: can't subclass sha256
  48. TYPE = 0x00
  49. def __init__(self, key, data=b''):
  50. # signature is like for a MAC, we ignore the key as this is a simple hash
  51. if key is not None:
  52. raise Exception("use a HMAC if you have a key")
  53. self.h = sha256(data)
  54. def update(self, data):
  55. self.h.update(data)
  56. def digest(self):
  57. return self.h.digest()
  58. def hexdigest(self):
  59. return self.h.hexdigest()
  60. class SHA512_256(sha512_256):
  61. """sha512, but digest truncated to 256bit - faster than sha256 on 64bit platforms"""
  62. TYPE = 0x01
  63. def __init__(self, key, data):
  64. # signature is like for a MAC, we ignore the key as this is a simple hash
  65. if key is not None:
  66. raise Exception("use a HMAC if you have a key")
  67. super().__init__(data)
  68. HASH_DEFAULT = SHA256.TYPE
  69. class HMAC_SHA256(HMAC):
  70. TYPE = 0x02
  71. def __init__(self, key, data):
  72. if key is None:
  73. raise Exception("do not use HMAC if you don't have a key")
  74. super().__init__(key, data, sha256)
  75. class HMAC_SHA512_256(HMAC):
  76. TYPE = 0x03
  77. def __init__(self, key, data):
  78. if key is None:
  79. raise Exception("do not use HMAC if you don't have a key")
  80. super().__init__(key, data, sha512_256)
  81. MAC_DEFAULT = HMAC_SHA256.TYPE
  82. class ZlibCompressor(object): # uses 0..9 in the mapping
  83. TYPE = 0
  84. LEVELS = range(10)
  85. def compress(self, data):
  86. level = self.TYPE - ZlibCompressor.TYPE
  87. return zlib.compress(data, level)
  88. def decompress(self, data):
  89. return zlib.decompress(data)
  90. class LzmaCompressor(object): # uses 10..19 in the mapping
  91. TYPE = 10
  92. PRESETS = range(10)
  93. def __init__(self):
  94. if lzma is None:
  95. raise NotImplemented("lzma compression needs Python >= 3.3 or backports.lzma from PyPi")
  96. def compress(self, data):
  97. preset = self.TYPE - LzmaCompressor.TYPE
  98. return lzma.compress(data, preset=preset)
  99. def decompress(self, data):
  100. return lzma.decompress(data)
  101. COMPR_DEFAULT = ZlibCompressor.TYPE + 6 # zlib level 6
  102. Meta = namedtuple('Meta', 'compr_type, crypt_type, mac_type, hmac, iv, stored_iv')
  103. class KeyBase(object):
  104. TYPE = 0x00 # override in derived classes
  105. def __init__(self, compressor, maccer):
  106. self.compressor = compressor()
  107. self.maccer = maccer
  108. def id_hash(self, data):
  109. """Return HMAC hash using the "id" HMAC key
  110. """
  111. def encrypt(self, data):
  112. pass
  113. def decrypt(self, id, data):
  114. pass
  115. class PlaintextKey(KeyBase):
  116. TYPE = 0x02
  117. chunk_seed = 0
  118. @classmethod
  119. def create(cls, repository, args):
  120. print('Encryption NOT enabled.\nUse the "--encryption=passphrase|keyfile" to enable encryption.')
  121. compressor = compressor_creator(args)
  122. maccer = maccer_creator(args, cls)
  123. return cls(compressor, maccer)
  124. @classmethod
  125. def detect(cls, repository, manifest_data):
  126. meta, data, compressor, crypter, maccer = parser(manifest_data)
  127. return cls(compressor, maccer)
  128. def id_hash(self, data):
  129. return self.maccer(None, data).digest()
  130. def encrypt(self, data):
  131. meta = Meta(compr_type=self.compressor.TYPE, crypt_type=self.TYPE, mac_type=self.maccer.TYPE,
  132. hmac=None, iv=None, stored_iv=None)
  133. data = self.compressor.compress(data)
  134. return generate(meta, data)
  135. def decrypt(self, id, data):
  136. meta, data, compressor, crypter, maccer = parser(data)
  137. assert isinstance(self, crypter)
  138. assert self.maccer is maccer
  139. data = self.compressor.decompress(data)
  140. if id and self.id_hash(data) != id:
  141. raise IntegrityError('Chunk id verification failed')
  142. return data
  143. class AESKeyBase(KeyBase):
  144. """Common base class shared by KeyfileKey and PassphraseKey
  145. Chunks are encrypted using 256bit AES in Counter Mode (CTR)
  146. Payload layout: HEADER(4) + HMAC(32) + NONCE(8) + CIPHERTEXT
  147. To reduce payload size only 8 bytes of the 16 bytes nonce is saved
  148. in the payload, the first 8 bytes are always zeros. This does not
  149. affect security but limits the maximum repository capacity to
  150. only 295 exabytes!
  151. """
  152. PAYLOAD_OVERHEAD = 4 + 32 + 8 # HEADER + HMAC + NONCE, TODO: get rid of this
  153. def id_hash(self, data):
  154. """Return HMAC hash using the "id" HMAC key
  155. """
  156. return self.maccer(self.id_key, data).digest()
  157. def encrypt(self, data):
  158. data = self.compressor.compress(data)
  159. self.enc_cipher.reset()
  160. stored_iv = self.enc_cipher.iv[8:]
  161. iv = PREFIX + stored_iv
  162. data = self.enc_cipher.encrypt(data)
  163. hmac = self.maccer(self.enc_hmac_key, stored_iv + data).digest()
  164. meta = Meta(compr_type=self.compressor.TYPE, crypt_type=self.TYPE, mac_type=self.maccer.TYPE,
  165. hmac=hmac, iv=iv, stored_iv=stored_iv)
  166. return generate(meta, data)
  167. def decrypt(self, id, data):
  168. meta, data, compressor, crypter, maccer = parser(data)
  169. assert isinstance(self, crypter)
  170. assert self.maccer is maccer
  171. computed_hmac = self.maccer(self.enc_hmac_key, meta.stored_iv + data).digest()
  172. if computed_hmac != meta.hmac:
  173. raise IntegrityError('Encryption envelope checksum mismatch')
  174. self.dec_cipher.reset(iv=meta.iv)
  175. data = self.compressor.decompress(self.dec_cipher.decrypt(data))
  176. if id and self.id_hash(data) != id:
  177. raise IntegrityError('Chunk id verification failed')
  178. return data
  179. def extract_nonce(self, payload):
  180. meta, data, compressor, crypter, maccer = parser(payload)
  181. assert isinstance(self, crypter)
  182. nonce = bytes_to_long(meta.stored_iv)
  183. return nonce
  184. def init_from_random_data(self, data):
  185. self.enc_key = data[0:32]
  186. self.enc_hmac_key = data[32:64]
  187. self.id_key = data[64:96]
  188. self.chunk_seed = bytes_to_int(data[96:100])
  189. # Convert to signed int32
  190. if self.chunk_seed & 0x80000000:
  191. self.chunk_seed = self.chunk_seed - 0xffffffff - 1
  192. def init_ciphers(self, enc_iv=b''):
  193. self.enc_cipher = AES(self.enc_key, enc_iv)
  194. self.dec_cipher = AES(self.enc_key)
  195. class PassphraseKey(AESKeyBase):
  196. TYPE = 0x01
  197. iterations = 100000
  198. @classmethod
  199. def create(cls, repository, args):
  200. compressor = compressor_creator(args)
  201. maccer = maccer_creator(args, cls)
  202. key = cls(compressor, maccer)
  203. passphrase = os.environ.get('ATTIC_PASSPHRASE')
  204. if passphrase is not None:
  205. passphrase2 = passphrase
  206. else:
  207. passphrase, passphrase2 = 1, 2
  208. while passphrase != passphrase2:
  209. passphrase = getpass('Enter passphrase: ')
  210. if not passphrase:
  211. print('Passphrase must not be blank')
  212. continue
  213. passphrase2 = getpass('Enter same passphrase again: ')
  214. if passphrase != passphrase2:
  215. print('Passphrases do not match')
  216. key.init(repository, passphrase)
  217. if passphrase:
  218. print('Remember your passphrase. Your data will be inaccessible without it.')
  219. return key
  220. @classmethod
  221. def detect(cls, repository, manifest_data):
  222. prompt = 'Enter passphrase for %s: ' % repository._location.orig
  223. meta, data, compressor, crypter, maccer = parser(manifest_data)
  224. key = cls(compressor, maccer)
  225. passphrase = os.environ.get('ATTIC_PASSPHRASE')
  226. if passphrase is None:
  227. passphrase = getpass(prompt)
  228. while True:
  229. key.init(repository, passphrase)
  230. try:
  231. key.decrypt(None, manifest_data)
  232. num_blocks = num_aes_blocks(len(data))
  233. key.init_ciphers(PREFIX + long_to_bytes(key.extract_nonce(manifest_data) + num_blocks))
  234. return key
  235. except IntegrityError:
  236. passphrase = getpass(prompt)
  237. def init(self, repository, passphrase):
  238. self.init_from_random_data(pbkdf2_sha256(passphrase.encode('utf-8'), repository.id, self.iterations, 100))
  239. self.init_ciphers()
  240. class KeyfileKey(AESKeyBase):
  241. FILE_ID = 'ATTIC KEY'
  242. TYPE = 0x00
  243. @classmethod
  244. def detect(cls, repository, manifest_data):
  245. meta, data, compressor, crypter, maccer = parser(manifest_data)
  246. key = cls(compressor, maccer)
  247. path = cls.find_key_file(repository)
  248. prompt = 'Enter passphrase for key file %s: ' % path
  249. passphrase = os.environ.get('ATTIC_PASSPHRASE', '')
  250. while not key.load(path, passphrase):
  251. passphrase = getpass(prompt)
  252. num_blocks = num_aes_blocks(len(data))
  253. key.init_ciphers(PREFIX + long_to_bytes(key.extract_nonce(manifest_data) + num_blocks))
  254. return key
  255. @classmethod
  256. def find_key_file(cls, repository):
  257. id = hexlify(repository.id).decode('ascii')
  258. keys_dir = get_keys_dir()
  259. for name in os.listdir(keys_dir):
  260. filename = os.path.join(keys_dir, name)
  261. with open(filename, 'r') as fd:
  262. line = fd.readline().strip()
  263. if line and line.startswith(cls.FILE_ID) and line[10:] == id:
  264. return filename
  265. raise Exception('Key file for repository with ID %s not found' % id)
  266. def load(self, filename, passphrase):
  267. with open(filename, 'r') as fd:
  268. cdata = a2b_base64(''.join(fd.readlines()[1:]).encode('ascii')) # .encode needed for Python 3.[0-2]
  269. data = self.decrypt_key_file(cdata, passphrase)
  270. if data:
  271. key = msgpack.unpackb(data)
  272. if key[b'version'] != 1:
  273. raise IntegrityError('Invalid key file header')
  274. self.repository_id = key[b'repository_id']
  275. self.enc_key = key[b'enc_key']
  276. self.enc_hmac_key = key[b'enc_hmac_key']
  277. self.id_key = key[b'id_key']
  278. self.chunk_seed = key[b'chunk_seed']
  279. self.path = filename
  280. return True
  281. def decrypt_key_file(self, data, passphrase):
  282. d = msgpack.unpackb(data)
  283. assert d[b'version'] == 1
  284. assert d[b'algorithm'] == b'sha256'
  285. key = pbkdf2_sha256(passphrase.encode('utf-8'), d[b'salt'], d[b'iterations'], 32)
  286. data = AES(key).decrypt(d[b'data'])
  287. if HMAC(key, data, sha256).digest() != d[b'hash']:
  288. return None
  289. return data
  290. def encrypt_key_file(self, data, passphrase):
  291. salt = get_random_bytes(32)
  292. iterations = 100000
  293. key = pbkdf2_sha256(passphrase.encode('utf-8'), salt, iterations, 32)
  294. hash = HMAC(key, data, sha256).digest()
  295. cdata = AES(key).encrypt(data)
  296. d = {
  297. 'version': 1,
  298. 'salt': salt,
  299. 'iterations': iterations,
  300. 'algorithm': 'sha256',
  301. 'hash': hash,
  302. 'data': cdata,
  303. }
  304. return msgpack.packb(d)
  305. def save(self, path, passphrase):
  306. key = {
  307. 'version': 1,
  308. 'repository_id': self.repository_id,
  309. 'enc_key': self.enc_key,
  310. 'enc_hmac_key': self.enc_hmac_key,
  311. 'id_key': self.id_key,
  312. 'chunk_seed': self.chunk_seed,
  313. }
  314. data = self.encrypt_key_file(msgpack.packb(key), passphrase)
  315. with open(path, 'w') as fd:
  316. fd.write('%s %s\n' % (self.FILE_ID, hexlify(self.repository_id).decode('ascii')))
  317. fd.write('\n'.join(textwrap.wrap(b2a_base64(data).decode('ascii'))))
  318. fd.write('\n')
  319. self.path = path
  320. def change_passphrase(self):
  321. passphrase, passphrase2 = 1, 2
  322. while passphrase != passphrase2:
  323. passphrase = getpass('New passphrase: ')
  324. passphrase2 = getpass('Enter same passphrase again: ')
  325. if passphrase != passphrase2:
  326. print('Passphrases do not match')
  327. self.save(self.path, passphrase)
  328. print('Key file "%s" updated' % self.path)
  329. @classmethod
  330. def create(cls, repository, args):
  331. filename = args.repository.to_key_filename()
  332. path = filename
  333. i = 1
  334. while os.path.exists(path):
  335. i += 1
  336. path = filename + '.%d' % i
  337. passphrase = os.environ.get('ATTIC_PASSPHRASE')
  338. if passphrase is not None:
  339. passphrase2 = passphrase
  340. else:
  341. passphrase, passphrase2 = 1, 2
  342. while passphrase != passphrase2:
  343. passphrase = getpass('Enter passphrase (empty for no passphrase):')
  344. passphrase2 = getpass('Enter same passphrase again: ')
  345. if passphrase != passphrase2:
  346. print('Passphrases do not match')
  347. compressor = compressor_creator(args)
  348. maccer = maccer_creator(args, cls)
  349. key = cls(compressor, maccer)
  350. key.repository_id = repository.id
  351. key.init_from_random_data(get_random_bytes(100))
  352. key.init_ciphers()
  353. key.save(path, passphrase)
  354. print('Key file "%s" created.' % key.path)
  355. print('Keep this file safe. Your data will be inaccessible without it.')
  356. return key
  357. # note: key 0 nicely maps to a zlib compressor with level 0 which means "no compression"
  358. compressor_mapping = {}
  359. for level in ZlibCompressor.LEVELS:
  360. compressor_mapping[ZlibCompressor.TYPE + level] = \
  361. type('ZlibCompressorLevel%d' % level, (ZlibCompressor, ), dict(TYPE=ZlibCompressor.TYPE + level))
  362. for preset in LzmaCompressor.PRESETS:
  363. compressor_mapping[LzmaCompressor.TYPE + preset] = \
  364. type('LzmaCompressorPreset%d' % preset, (LzmaCompressor, ), dict(TYPE=LzmaCompressor.TYPE + preset))
  365. crypter_mapping = {
  366. KeyfileKey.TYPE: KeyfileKey,
  367. PassphraseKey.TYPE: PassphraseKey,
  368. PlaintextKey.TYPE: PlaintextKey,
  369. }
  370. maccer_mapping = {
  371. # simple hashes, not MACs (but MAC-like class __init__ method signature):
  372. SHA256.TYPE: SHA256,
  373. SHA512_256.TYPE: SHA512_256,
  374. # MACs:
  375. HMAC_SHA256.TYPE: HMAC_SHA256,
  376. HMAC_SHA512_256.TYPE: HMAC_SHA512_256,
  377. }
  378. def get_implementations(meta):
  379. try:
  380. compressor = compressor_mapping[meta.compr_type]
  381. crypter = crypter_mapping[meta.crypt_type]
  382. maccer = maccer_mapping[meta.mac_type]
  383. except KeyError:
  384. raise UnsupportedPayloadError("compr_type %x crypt_type %x mac_type %x" % (
  385. meta.compr_type, meta.crypt_type, meta.mac_type))
  386. return compressor, crypter, maccer
  387. def legacy_parser(all_data, crypt_type): # all rather hardcoded
  388. offset = 1
  389. if crypt_type == PlaintextKey.TYPE:
  390. hmac = None
  391. iv = stored_iv = None
  392. data = all_data[offset:]
  393. else:
  394. hmac = all_data[offset:offset+32]
  395. stored_iv = all_data[offset+32:offset+40]
  396. iv = PREFIX + stored_iv
  397. data = all_data[offset+40:]
  398. meta = Meta(compr_type=6, crypt_type=crypt_type, mac_type=HMAC_SHA256.TYPE,
  399. hmac=hmac, iv=iv, stored_iv=stored_iv)
  400. compressor, crypter, maccer = get_implementations(meta)
  401. return meta, data, compressor, crypter, maccer
  402. def parser00(all_data):
  403. return legacy_parser(all_data, KeyfileKey.TYPE)
  404. def parser01(all_data):
  405. return legacy_parser(all_data, PassphraseKey.TYPE)
  406. def parser02(all_data):
  407. return legacy_parser(all_data, PlaintextKey.TYPE)
  408. def parser03(all_data): # new & flexible
  409. offset = 4
  410. compr_type, crypt_type, mac_type = all_data[1:offset]
  411. if crypt_type == PlaintextKey.TYPE:
  412. hmac = None
  413. iv = stored_iv = None
  414. data = all_data[offset:]
  415. else:
  416. hmac = all_data[offset:offset+32]
  417. stored_iv = all_data[offset+32:offset+40]
  418. iv = PREFIX + stored_iv
  419. data = all_data[offset+40:]
  420. meta = Meta(compr_type=compr_type, crypt_type=crypt_type, mac_type=mac_type,
  421. hmac=hmac, iv=iv, stored_iv=stored_iv)
  422. compressor, crypter, maccer = get_implementations(meta)
  423. return meta, data, compressor, crypter, maccer
  424. def parser(data):
  425. parser_mapping = {
  426. 0x00: parser00,
  427. 0x01: parser01,
  428. 0x02: parser02,
  429. 0x03: parser03,
  430. }
  431. header_type = data[0]
  432. parser_func = parser_mapping[header_type]
  433. return parser_func(data)
  434. def key_factory(repository, manifest_data):
  435. meta, data, compressor, crypter, maccer = parser(manifest_data)
  436. return crypter.detect(repository, manifest_data)
  437. def generate(meta, data):
  438. # always create new-style 0x03 format
  439. start = bytes([0x03, meta.compr_type, meta.crypt_type, meta.mac_type])
  440. if meta.crypt_type == PlaintextKey.TYPE:
  441. result = start + data
  442. else:
  443. assert len(meta.hmac) == 32
  444. assert len(meta.stored_iv) == 8
  445. result = start + meta.hmac + meta.stored_iv + data
  446. return result
  447. def compressor_creator(args):
  448. # args == None is used by unit tests
  449. compression = COMPR_DEFAULT if args is None else args.compression
  450. compressor = compressor_mapping.get(compression)
  451. if compressor is None:
  452. raise NotImplementedError("no compression %d" % args.compression)
  453. return compressor
  454. def key_creator(repository, args):
  455. if args.encryption == 'keyfile':
  456. return KeyfileKey.create(repository, args)
  457. if args.encryption == 'passphrase':
  458. return PassphraseKey.create(repository, args)
  459. if args.encryption == 'none':
  460. return PlaintextKey.create(repository, args)
  461. raise NotImplemented("no encryption %s" % args.encryption)
  462. def maccer_creator(args, key_cls):
  463. # args == None is used by unit tests
  464. mac = None if args is None else args.mac
  465. if mac is None:
  466. if key_cls is PlaintextKey:
  467. mac = HASH_DEFAULT
  468. elif key_cls in (KeyfileKey, PassphraseKey):
  469. mac = MAC_DEFAULT
  470. else:
  471. raise NotImplementedError("unknown key class")
  472. maccer = maccer_mapping.get(mac)
  473. if maccer is None:
  474. raise NotImplementedError("no mac %d" % args.mac)
  475. return maccer