key.py 28 KB

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