key.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  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, AES_CTR_MODE, AES_GCM_MODE, \
  18. bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks
  19. from attic.helpers import IntegrityError, get_keys_dir, Error
  20. # we do not store the full IV on disk, as the upper 8 bytes are expected to be
  21. # zero anyway as the full IV is a 128bit counter. PREFIX are the upper 8 bytes,
  22. # stored_iv are the lower 8 Bytes.
  23. PREFIX = b'\0' * 8
  24. Meta = namedtuple('Meta', 'compr_type, key_type, mac_type, cipher_type, iv, legacy')
  25. class UnsupportedPayloadError(Error):
  26. """Unsupported payload type {}. A newer version is required to access this repository.
  27. """
  28. class sha512_256(object): # note: can't subclass sha512
  29. """sha512, but digest truncated to 256bit - faster than sha256 on 64bit platforms"""
  30. digestsize = digest_size = 32
  31. block_size = 64
  32. def __init__(self, data=None):
  33. self.name = 'sha512-256'
  34. self._h = sha512()
  35. if data:
  36. self.update(data)
  37. def update(self, data):
  38. self._h.update(data)
  39. def digest(self):
  40. return self._h.digest()[:self.digest_size]
  41. def hexdigest(self):
  42. return self._h.hexdigest()[:self.digest_size * 2]
  43. def copy(self):
  44. new = sha512_256.__new__(sha512_256)
  45. new._h = self._h.copy()
  46. return new
  47. class HMAC(hmac.HMAC):
  48. """Workaround a bug in Python < 3.4 Where HMAC does not accept memoryviews
  49. """
  50. def update(self, msg):
  51. self.inner.update(msg)
  52. # HASH / MAC stuff below all has a mac-like interface, so it can be used in the same way.
  53. # special case: hashes do not use keys (and thus, do not sign/authenticate)
  54. class SHA256(object): # note: can't subclass sha256
  55. TYPE = 0
  56. digest_size = 32
  57. def __init__(self, key, data=b''):
  58. # signature is like for a MAC, we ignore the key as this is a simple hash
  59. if key is not None:
  60. raise Exception("use a HMAC if you have a key")
  61. self.h = sha256(data)
  62. def update(self, data):
  63. self.h.update(data)
  64. def digest(self):
  65. return self.h.digest()
  66. def hexdigest(self):
  67. return self.h.hexdigest()
  68. class SHA512_256(sha512_256):
  69. """sha512, but digest truncated to 256bit - faster than sha256 on 64bit platforms"""
  70. TYPE = 1
  71. digest_size = 32
  72. def __init__(self, key, data):
  73. # signature is like for a MAC, we ignore the key as this is a simple hash
  74. if key is not None:
  75. raise Exception("use a HMAC if you have a key")
  76. super().__init__(data)
  77. class GHASH:
  78. TYPE = 2
  79. digest_size = 16
  80. def __init__(self, key, data):
  81. # signature is like for a MAC, we ignore the key as this is a simple hash
  82. if key is not None:
  83. raise Exception("use a MAC if you have a key")
  84. self.key = b'\0' * 32
  85. self.data = data
  86. def digest(self):
  87. mac_cipher = AES(mode=AES_GCM_MODE, is_encrypt=True, key=self.key, iv=b'\0' * 16)
  88. # GMAC = aes-gcm with all data as AAD, no data as to-be-encrypted data
  89. mac_cipher.add(bytes(self.data))
  90. hash, _ = mac_cipher.compute_mac_and_encrypt(b'')
  91. return hash
  92. class HMAC_SHA256(HMAC):
  93. TYPE = 10
  94. digest_size = 32
  95. def __init__(self, key, data):
  96. if key is None:
  97. raise Exception("do not use HMAC if you don't have a key")
  98. super().__init__(key, data, sha256)
  99. class HMAC_SHA512_256(HMAC):
  100. TYPE = 11
  101. digest_size = 32
  102. def __init__(self, key, data):
  103. if key is None:
  104. raise Exception("do not use HMAC if you don't have a key")
  105. super().__init__(key, data, sha512_256)
  106. class GMAC(GHASH):
  107. TYPE = 20
  108. digest_size = 16
  109. def __init__(self, key, data):
  110. super().__init__(None, data)
  111. if key is None:
  112. raise Exception("do not use GMAC if you don't have a key")
  113. self.key = key
  114. # defaults are optimized for speed on modern CPUs with AES hw support
  115. HASH_DEFAULT = GHASH.TYPE
  116. MAC_DEFAULT = GMAC.TYPE
  117. # compressor classes, all same interface
  118. # special case: zlib level 0 is "no compression"
  119. class NullCompressor(object): # uses 0 in the mapping
  120. TYPE = 0
  121. def compress(self, data):
  122. return bytes(data)
  123. def decompress(self, data):
  124. return bytes(data)
  125. class ZlibCompressor(object): # uses 1..9 in the mapping
  126. TYPE = 0
  127. LEVELS = range(10)
  128. def compress(self, data):
  129. level = self.TYPE - ZlibCompressor.TYPE
  130. return zlib.compress(data, level)
  131. def decompress(self, data):
  132. return zlib.decompress(data)
  133. class LzmaCompressor(object): # uses 10..19 in the mapping
  134. TYPE = 10
  135. PRESETS = range(10)
  136. def __init__(self):
  137. if lzma is None:
  138. raise NotImplemented("lzma compression needs Python >= 3.3 or backports.lzma from PyPi")
  139. def compress(self, data):
  140. preset = self.TYPE - LzmaCompressor.TYPE
  141. return lzma.compress(data, preset=preset)
  142. def decompress(self, data):
  143. return lzma.decompress(data)
  144. # default is optimized for speed
  145. COMPR_DEFAULT = NullCompressor.TYPE # no compression
  146. # ciphers - AEAD (authenticated encryption with assoc. data) style interface
  147. # special case: PLAIN dummy does not encrypt / authenticate
  148. class PLAIN:
  149. TYPE = 0
  150. enc_iv = None # dummy
  151. def __init__(self, **kw):
  152. pass
  153. def compute_mac_and_encrypt(self, meta, data):
  154. return None, data
  155. def check_mac_and_decrypt(self, mac, meta, data):
  156. return data
  157. def increment_iv(iv, amount):
  158. """
  159. increment the given IV considering that <amount> bytes of data was
  160. encrypted based on it. In CTR / GCM mode, the IV is just a counter and
  161. must never repeat.
  162. :param iv: current IV, 16 bytes (128 bit)
  163. :param amount: amount of data (in bytes) that was encrypted
  164. :return: new IV, 16 bytes (128 bit)
  165. """
  166. # TODO: code assumes that the last 8 bytes are enough, the upper 8 always zero
  167. iv_last8 = iv[8:]
  168. current_iv = bytes_to_long(iv_last8)
  169. new_iv = current_iv + num_aes_blocks(amount)
  170. iv_last8 = long_to_bytes(new_iv)
  171. iv = PREFIX + iv_last8
  172. return iv
  173. def get_aad(meta):
  174. """get additional authenticated data for AEAD ciphers"""
  175. if meta.legacy:
  176. # legacy format computed the mac over (iv_last8 + data)
  177. return meta.iv[8:]
  178. else:
  179. return msgpack.packb(meta)
  180. class AES_CTR_HMAC:
  181. TYPE = 1
  182. def __init__(self, enc_key=b'\0' * 32, enc_iv=b'\0' * 16, enc_hmac_key=b'\0' * 32, **kw):
  183. self.hmac_key = enc_hmac_key
  184. self.enc_iv = enc_iv
  185. self.enc_cipher = AES(mode=AES_CTR_MODE, is_encrypt=True, key=enc_key, iv=enc_iv)
  186. self.dec_cipher = AES(mode=AES_CTR_MODE, is_encrypt=False, key=enc_key)
  187. def compute_mac_and_encrypt(self, meta, data):
  188. self.enc_cipher.reset(iv=meta.iv)
  189. _, data = self.enc_cipher.compute_mac_and_encrypt(data)
  190. self.enc_iv = increment_iv(meta.iv, len(data))
  191. aad = get_aad(meta)
  192. mac = HMAC(self.hmac_key, aad + data, sha256).digest() # XXX mac / hash flexibility
  193. return mac, data
  194. def check_mac_and_decrypt(self, mac, meta, data):
  195. aad = get_aad(meta)
  196. if HMAC(self.hmac_key, aad + data, sha256).digest() != mac:
  197. raise IntegrityError('Encryption envelope checksum mismatch')
  198. self.dec_cipher.reset(iv=meta.iv)
  199. data = self.dec_cipher.check_mac_and_decrypt(None, data)
  200. return data
  201. class AES_GCM:
  202. TYPE = 2
  203. def __init__(self, enc_key=b'\0' * 32, enc_iv=b'\0' * 16, **kw):
  204. # note: hmac_key is not used for aes-gcm, it does aes+gmac in 1 pass
  205. self.enc_iv = enc_iv
  206. self.enc_cipher = AES(mode=AES_GCM_MODE, is_encrypt=True, key=enc_key, iv=enc_iv)
  207. self.dec_cipher = AES(mode=AES_GCM_MODE, is_encrypt=False, key=enc_key)
  208. def compute_mac_and_encrypt(self, meta, data):
  209. self.enc_cipher.reset(iv=meta.iv)
  210. aad = get_aad(meta)
  211. self.enc_cipher.add(aad)
  212. mac, data = self.enc_cipher.compute_mac_and_encrypt(data)
  213. self.enc_iv = increment_iv(meta.iv, len(data))
  214. return mac, data
  215. def check_mac_and_decrypt(self, mac, meta, data):
  216. self.dec_cipher.reset(iv=meta.iv)
  217. aad = get_aad(meta)
  218. self.dec_cipher.add(aad)
  219. try:
  220. data = self.dec_cipher.check_mac_and_decrypt(mac, data)
  221. except Exception:
  222. raise IntegrityError('Encryption envelope checksum mismatch')
  223. return data
  224. # cipher default is optimized for speed on modern CPUs with AES hw support
  225. PLAIN_DEFAULT = PLAIN.TYPE
  226. CIPHER_DEFAULT = AES_GCM.TYPE
  227. # misc. types of keys
  228. # special case: no keys (thus: no encryption, no signing/authentication)
  229. class KeyBase(object):
  230. TYPE = 0x00 # override in derived classes
  231. def __init__(self, compressor_cls, maccer_cls, cipher_cls):
  232. self.compressor = compressor_cls()
  233. self.maccer_cls = maccer_cls # hasher/maccer used by id_hash
  234. self.cipher_cls = cipher_cls # plaintext dummy or AEAD cipher
  235. self.cipher = cipher_cls()
  236. self.id_key = None
  237. def id_hash(self, data):
  238. """Return a HASH (no id_key) or a MAC (using the "id_key" key)
  239. XXX do we need a cryptographic hash function here or is a keyed hash
  240. function like GMAC / GHASH good enough? See NIST SP 800-38D.
  241. IMPORTANT: in 1 repo, there should be only 1 kind of id_hash, otherwise
  242. data hashed/maced with one id_hash might result in same ID as already
  243. exists in the repo for other data created with another id_hash method.
  244. somehow unlikely considering 128 or 256bits, but still.
  245. """
  246. return self.maccer_cls(self.id_key, data).digest()
  247. def encrypt(self, data):
  248. data = self.compressor.compress(data)
  249. meta = Meta(compr_type=self.compressor.TYPE, key_type=self.TYPE,
  250. mac_type=self.maccer_cls.TYPE, cipher_type=self.cipher.TYPE,
  251. iv=self.cipher.enc_iv, legacy=False)
  252. mac, data = self.cipher.compute_mac_and_encrypt(meta, data)
  253. return generate(mac, meta, data)
  254. def decrypt(self, id, data):
  255. mac, meta, data = parser(data)
  256. compressor, keyer, maccer, cipher = get_implementations(meta)
  257. assert isinstance(self, keyer)
  258. assert self.maccer_cls is maccer
  259. assert self.cipher_cls is cipher
  260. data = self.cipher.check_mac_and_decrypt(mac, meta, data)
  261. data = self.compressor.decompress(data)
  262. if id and self.id_hash(data) != id:
  263. raise IntegrityError('Chunk id verification failed')
  264. return data
  265. class PlaintextKey(KeyBase):
  266. TYPE = 0x02
  267. chunk_seed = 0
  268. @classmethod
  269. def create(cls, repository, args):
  270. print('Encryption NOT enabled.\nUse the "--encryption=passphrase|keyfile" to enable encryption.')
  271. compressor = compressor_creator(args)
  272. maccer = maccer_creator(args, cls)
  273. cipher = cipher_creator(args, cls)
  274. return cls(compressor, maccer, cipher)
  275. @classmethod
  276. def detect(cls, repository, manifest_data):
  277. mac, meta, data = parser(manifest_data)
  278. compressor, keyer, maccer, cipher = get_implementations(meta)
  279. return cls(compressor, maccer, cipher)
  280. class AESKeyBase(KeyBase):
  281. """Common base class shared by KeyfileKey and PassphraseKey
  282. Chunks are encrypted using 256bit AES in CTR or GCM mode.
  283. Chunks are authenticated by a GCM GMAC or a HMAC.
  284. Payload layout: TYPE(1) + MAC(32) + NONCE(8) + CIPHERTEXT
  285. To reduce payload size only 8 bytes of the 16 bytes nonce is saved
  286. in the payload, the first 8 bytes are always zeros. This does not
  287. affect security but limits the maximum repository capacity to
  288. only 295 exabytes!
  289. """
  290. def extract_iv(self, payload):
  291. _, meta, _ = parser(payload)
  292. return meta.iv
  293. def init_from_random_data(self, data):
  294. self.enc_key = data[0:32]
  295. self.enc_hmac_key = data[32:64]
  296. self.id_key = data[64:96]
  297. self.chunk_seed = bytes_to_int(data[96:100])
  298. # Convert to signed int32
  299. if self.chunk_seed & 0x80000000:
  300. self.chunk_seed = self.chunk_seed - 0xffffffff - 1
  301. def init_ciphers(self, enc_iv=b'\0' * 16):
  302. self.cipher = self.cipher_cls(enc_key=self.enc_key, enc_iv=enc_iv,
  303. enc_hmac_key=self.enc_hmac_key)
  304. @property
  305. def enc_iv(self):
  306. return self.cipher.enc_iv
  307. class PassphraseKey(AESKeyBase):
  308. TYPE = 0x01
  309. iterations = 100000
  310. @classmethod
  311. def create(cls, repository, args):
  312. compressor = compressor_creator(args)
  313. maccer = maccer_creator(args, cls)
  314. cipher = cipher_creator(args, cls)
  315. key = cls(compressor, maccer, cipher)
  316. passphrase = os.environ.get('ATTIC_PASSPHRASE')
  317. if passphrase is not None:
  318. passphrase2 = passphrase
  319. else:
  320. passphrase, passphrase2 = 1, 2
  321. while passphrase != passphrase2:
  322. passphrase = getpass('Enter passphrase: ')
  323. if not passphrase:
  324. print('Passphrase must not be blank')
  325. continue
  326. passphrase2 = getpass('Enter same passphrase again: ')
  327. if passphrase != passphrase2:
  328. print('Passphrases do not match')
  329. key.init(repository, passphrase)
  330. if passphrase:
  331. print('Remember your passphrase. Your data will be inaccessible without it.')
  332. return key
  333. @classmethod
  334. def detect(cls, repository, manifest_data):
  335. prompt = 'Enter passphrase for %s: ' % repository._location.orig
  336. mac, meta, data = parser(manifest_data)
  337. compressor, keyer, maccer, cipher = get_implementations(meta)
  338. key = cls(compressor, maccer, cipher)
  339. passphrase = os.environ.get('ATTIC_PASSPHRASE')
  340. if passphrase is None:
  341. passphrase = getpass(prompt)
  342. while True:
  343. key.init(repository, passphrase)
  344. try:
  345. key.decrypt(None, manifest_data)
  346. key.init_ciphers(increment_iv(key.extract_iv(manifest_data), len(data)))
  347. return key
  348. except IntegrityError:
  349. passphrase = getpass(prompt)
  350. def change_passphrase(self):
  351. class ImmutablePassphraseError(Error):
  352. """The passphrase for this encryption key type can't be changed."""
  353. raise ImmutablePassphraseError
  354. def init(self, repository, passphrase):
  355. self.init_from_random_data(pbkdf2_sha256(passphrase.encode('utf-8'), repository.id, self.iterations, 100))
  356. self.init_ciphers()
  357. class KeyfileKey(AESKeyBase):
  358. FILE_ID = 'ATTIC KEY'
  359. TYPE = 0x00
  360. @classmethod
  361. def detect(cls, repository, manifest_data):
  362. mac, meta, data = parser(manifest_data)
  363. compressor, keyer, maccer, cipher = get_implementations(meta)
  364. key = cls(compressor, maccer, cipher)
  365. path = cls.find_key_file(repository)
  366. prompt = 'Enter passphrase for key file %s: ' % path
  367. passphrase = os.environ.get('ATTIC_PASSPHRASE', '')
  368. while not key.load(path, passphrase):
  369. passphrase = getpass(prompt)
  370. key.init_ciphers(increment_iv(key.extract_iv(manifest_data), len(data)))
  371. return key
  372. @classmethod
  373. def find_key_file(cls, repository):
  374. id = hexlify(repository.id).decode('ascii')
  375. keys_dir = get_keys_dir()
  376. for name in os.listdir(keys_dir):
  377. filename = os.path.join(keys_dir, name)
  378. with open(filename, 'r') as fd:
  379. line = fd.readline().strip()
  380. if line and line.startswith(cls.FILE_ID) and line[10:] == id:
  381. return filename
  382. raise Exception('Key file for repository with ID %s not found' % id)
  383. def load(self, filename, passphrase):
  384. with open(filename, 'r') as fd:
  385. cdata = a2b_base64(''.join(fd.readlines()[1:]).encode('ascii')) # .encode needed for Python 3.[0-2]
  386. data = self.decrypt_key_file(cdata, passphrase)
  387. if data:
  388. key = msgpack.unpackb(data)
  389. if key[b'version'] != 1:
  390. raise IntegrityError('Invalid key file header')
  391. self.repository_id = key[b'repository_id']
  392. self.enc_key = key[b'enc_key']
  393. self.enc_hmac_key = key[b'enc_hmac_key']
  394. self.id_key = key[b'id_key']
  395. self.chunk_seed = key[b'chunk_seed']
  396. self.path = filename
  397. return True
  398. def decrypt_key_file(self, data, passphrase):
  399. d = msgpack.unpackb(data)
  400. assert d[b'version'] == 1
  401. assert d[b'algorithm'] == b'gmac'
  402. key = pbkdf2_sha256(passphrase.encode('utf-8'), d[b'salt'], d[b'iterations'], 32)
  403. try:
  404. cipher = AES(mode=AES_GCM_MODE, is_encrypt=False, key=key, iv=b'\0'*16)
  405. data = cipher.check_mac_and_decrypt(d[b'hash'], d[b'data'])
  406. return data
  407. except Exception:
  408. return None
  409. def encrypt_key_file(self, data, passphrase):
  410. salt = get_random_bytes(32)
  411. iterations = 100000
  412. key = pbkdf2_sha256(passphrase.encode('utf-8'), salt, iterations, 32)
  413. cipher = AES(mode=AES_GCM_MODE, is_encrypt=True, key=key, iv=b'\0'*16)
  414. mac, cdata = cipher.compute_mac_and_encrypt(data)
  415. d = {
  416. 'version': 1,
  417. 'salt': salt,
  418. 'iterations': iterations,
  419. 'algorithm': 'gmac',
  420. 'hash': mac,
  421. 'data': cdata,
  422. }
  423. return msgpack.packb(d)
  424. def save(self, path, passphrase):
  425. key = {
  426. 'version': 1,
  427. 'repository_id': self.repository_id,
  428. 'enc_key': self.enc_key,
  429. 'enc_hmac_key': self.enc_hmac_key,
  430. 'id_key': self.id_key,
  431. 'chunk_seed': self.chunk_seed,
  432. }
  433. data = self.encrypt_key_file(msgpack.packb(key), passphrase)
  434. with open(path, 'w') as fd:
  435. fd.write('%s %s\n' % (self.FILE_ID, hexlify(self.repository_id).decode('ascii')))
  436. fd.write('\n'.join(textwrap.wrap(b2a_base64(data).decode('ascii'))))
  437. fd.write('\n')
  438. self.path = path
  439. def change_passphrase(self):
  440. passphrase, passphrase2 = 1, 2
  441. while passphrase != passphrase2:
  442. passphrase = getpass('New passphrase: ')
  443. passphrase2 = getpass('Enter same passphrase again: ')
  444. if passphrase != passphrase2:
  445. print('Passphrases do not match')
  446. self.save(self.path, passphrase)
  447. print('Key file "%s" updated' % self.path)
  448. @classmethod
  449. def create(cls, repository, args):
  450. filename = args.repository.to_key_filename()
  451. path = filename
  452. i = 1
  453. while os.path.exists(path):
  454. i += 1
  455. path = filename + '.%d' % i
  456. passphrase = os.environ.get('ATTIC_PASSPHRASE')
  457. if passphrase is not None:
  458. passphrase2 = passphrase
  459. else:
  460. passphrase, passphrase2 = 1, 2
  461. while passphrase != passphrase2:
  462. passphrase = getpass('Enter passphrase (empty for no passphrase):')
  463. passphrase2 = getpass('Enter same passphrase again: ')
  464. if passphrase != passphrase2:
  465. print('Passphrases do not match')
  466. compressor = compressor_creator(args)
  467. maccer = maccer_creator(args, cls)
  468. cipher = cipher_creator(args, cls)
  469. key = cls(compressor, maccer, cipher)
  470. key.repository_id = repository.id
  471. key.init_from_random_data(get_random_bytes(100))
  472. key.init_ciphers()
  473. key.save(path, passphrase)
  474. print('Key file "%s" created.' % key.path)
  475. print('Keep this file safe. Your data will be inaccessible without it.')
  476. return key
  477. # note: key 0 nicely maps to a zlib compressor with level 0 which means "no compression"
  478. compressor_mapping = {}
  479. for level in ZlibCompressor.LEVELS:
  480. compressor_mapping[ZlibCompressor.TYPE + level] = \
  481. type('ZlibCompressorLevel%d' % level, (ZlibCompressor, ), dict(TYPE=ZlibCompressor.TYPE + level))
  482. for preset in LzmaCompressor.PRESETS:
  483. compressor_mapping[LzmaCompressor.TYPE + preset] = \
  484. type('LzmaCompressorPreset%d' % preset, (LzmaCompressor, ), dict(TYPE=LzmaCompressor.TYPE + preset))
  485. # overwrite 0 with NullCompressor
  486. compressor_mapping[NullCompressor.TYPE] = NullCompressor
  487. keyer_mapping = {
  488. KeyfileKey.TYPE: KeyfileKey,
  489. PassphraseKey.TYPE: PassphraseKey,
  490. PlaintextKey.TYPE: PlaintextKey,
  491. }
  492. maccer_mapping = {
  493. # simple hashes, not MACs (but MAC-like class __init__ method signature):
  494. SHA256.TYPE: SHA256,
  495. SHA512_256.TYPE: SHA512_256,
  496. GHASH.TYPE: GHASH,
  497. # MACs:
  498. HMAC_SHA256.TYPE: HMAC_SHA256,
  499. HMAC_SHA512_256.TYPE: HMAC_SHA512_256,
  500. GMAC.TYPE: GMAC,
  501. }
  502. cipher_mapping = {
  503. # no cipher (but cipher-like class __init__ method signature):
  504. PLAIN.TYPE: PLAIN,
  505. # AEAD cipher implementations
  506. AES_CTR_HMAC.TYPE: AES_CTR_HMAC,
  507. AES_GCM.TYPE: AES_GCM,
  508. }
  509. def get_implementations(meta):
  510. try:
  511. compressor = compressor_mapping[meta.compr_type]
  512. keyer = keyer_mapping[meta.key_type]
  513. maccer = maccer_mapping[meta.mac_type]
  514. cipher = cipher_mapping[meta.cipher_type]
  515. except KeyError:
  516. raise UnsupportedPayloadError("compr_type %x key_type %x mac_type %x cipher_type %x" % (
  517. meta.compr_type, meta.key_type, meta.mac_type, meta.cipher_type))
  518. return compressor, keyer, maccer, cipher
  519. def legacy_parser(all_data, key_type): # all rather hardcoded
  520. """
  521. Payload layout:
  522. no encryption: TYPE(1) + data
  523. with encryption: TYPE(1) + HMAC(32) + NONCE(8) + data
  524. data is compressed with zlib level 6 and (in the 2nd case) encrypted.
  525. To reduce payload size only 8 bytes of the 16 bytes nonce is saved
  526. in the payload, the first 8 bytes are always zeros. This does not
  527. affect security but limits the maximum repository capacity to
  528. only 295 exabytes!
  529. """
  530. offset = 1
  531. if key_type == PlaintextKey.TYPE:
  532. mac_type = SHA256.TYPE
  533. mac = None
  534. cipher_type = PLAIN.TYPE
  535. iv = None
  536. data = all_data[offset:]
  537. else:
  538. mac_type = HMAC_SHA256.TYPE
  539. mac = all_data[offset:offset+32]
  540. cipher_type = AES_CTR_HMAC.TYPE
  541. iv = PREFIX + all_data[offset+32:offset+40]
  542. data = all_data[offset+40:]
  543. meta = Meta(compr_type=6, key_type=key_type, mac_type=mac_type,
  544. cipher_type=cipher_type, iv=iv, legacy=True)
  545. return mac, meta, data
  546. def parser00(all_data):
  547. return legacy_parser(all_data, KeyfileKey.TYPE)
  548. def parser01(all_data):
  549. return legacy_parser(all_data, PassphraseKey.TYPE)
  550. def parser02(all_data):
  551. return legacy_parser(all_data, PlaintextKey.TYPE)
  552. def parser03(all_data): # new & flexible
  553. """
  554. Payload layout:
  555. always: TYPE(1) + MSGPACK((mac, meta, data))
  556. meta is a Meta namedtuple and contains all required information about data.
  557. data is maybe compressed (see meta) and maybe encrypted (see meta).
  558. """
  559. max_len = 10000000 # XXX formula?
  560. unpacker = msgpack.Unpacker(
  561. use_list=False,
  562. # avoid memory allocation issues causes by tampered input data.
  563. max_buffer_size=max_len, # does not work in 0.4.6 unpackb C implementation
  564. max_array_len=10, # meta_tuple
  565. max_bin_len=max_len, # data
  566. max_str_len=0, # not used yet
  567. max_map_len=0, # not used yet
  568. max_ext_len=0, # not used yet
  569. )
  570. unpacker.feed(all_data[1:])
  571. mac, meta_tuple, data = unpacker.unpack()
  572. meta = Meta(*meta_tuple)
  573. return mac, meta, data
  574. def parser(data):
  575. parser_mapping = {
  576. 0x00: parser00,
  577. 0x01: parser01,
  578. 0x02: parser02,
  579. 0x03: parser03,
  580. }
  581. header_type = data[0]
  582. parser_func = parser_mapping[header_type]
  583. return parser_func(data)
  584. def key_factory(repository, manifest_data):
  585. mac, meta, data = parser(manifest_data)
  586. compressor, keyer, maccer, cipher = get_implementations(meta)
  587. return keyer.detect(repository, manifest_data)
  588. def generate(mac, meta, data):
  589. # always create new-style 0x03 format
  590. return b'\x03' + msgpack.packb((mac, meta, data), use_bin_type=True)
  591. def compressor_creator(args):
  592. # args == None is used by unit tests
  593. compression = COMPR_DEFAULT if args is None else args.compression
  594. compressor = compressor_mapping.get(compression)
  595. if compressor is None:
  596. raise NotImplementedError("no compression %d" % args.compression)
  597. return compressor
  598. def key_creator(args):
  599. if args.encryption == 'keyfile':
  600. return KeyfileKey
  601. if args.encryption == 'passphrase':
  602. return PassphraseKey
  603. if args.encryption == 'none':
  604. return PlaintextKey
  605. raise NotImplemented("no encryption %s" % args.encryption)
  606. def maccer_creator(args, key_cls):
  607. # args == None is used by unit tests
  608. mac = None if args is None else args.mac
  609. if mac is None:
  610. if key_cls is PlaintextKey:
  611. mac = HASH_DEFAULT
  612. elif key_cls in (KeyfileKey, PassphraseKey):
  613. mac = MAC_DEFAULT
  614. else:
  615. raise NotImplementedError("unknown key class")
  616. maccer = maccer_mapping.get(mac)
  617. if maccer is None:
  618. raise NotImplementedError("no mac %d" % args.mac)
  619. return maccer
  620. def cipher_creator(args, key_cls):
  621. # args == None is used by unit tests
  622. cipher = None if args is None else args.cipher
  623. if cipher is None:
  624. if key_cls is PlaintextKey:
  625. cipher = PLAIN_DEFAULT
  626. elif key_cls in (KeyfileKey, PassphraseKey):
  627. cipher = CIPHER_DEFAULT
  628. else:
  629. raise NotImplementedError("unknown key class")
  630. cipher = cipher_mapping.get(cipher)
  631. if cipher is None:
  632. raise NotImplementedError("no cipher %d" % args.cipher)
  633. return cipher