key.py 23 KB

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