key.py 21 KB

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