2013-06-24 20:41:05 +00:00
|
|
|
from binascii import hexlify, a2b_base64, b2a_base64
|
2011-07-30 19:13:48 +00:00
|
|
|
from getpass import getpass
|
|
|
|
import os
|
|
|
|
import msgpack
|
2013-06-20 11:42:10 +00:00
|
|
|
import textwrap
|
2013-06-16 21:28:09 +00:00
|
|
|
import hmac
|
|
|
|
from hashlib import sha256
|
2011-07-30 19:13:48 +00:00
|
|
|
import zlib
|
|
|
|
|
2013-08-12 12:39:14 +00:00
|
|
|
from attic.crypto import pbkdf2_sha256, get_random_bytes, AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks
|
2014-12-17 20:55:33 +00:00
|
|
|
from attic.helpers import IntegrityError, get_keys_dir, Error
|
2011-07-30 19:13:48 +00:00
|
|
|
|
2013-06-03 11:45:48 +00:00
|
|
|
PREFIX = b'\0' * 8
|
2011-07-30 19:13:48 +00:00
|
|
|
|
2013-06-24 20:41:05 +00:00
|
|
|
|
2014-12-17 20:55:33 +00:00
|
|
|
class UnsupportedPayloadError(Error):
|
|
|
|
"""Unsupported payload type {}. A newer version is required to access this repository.
|
|
|
|
"""
|
|
|
|
|
2015-04-19 21:09:10 +00:00
|
|
|
class KeyfileNotFoundError(Error):
|
|
|
|
"""No key file for repository {} found in {}.
|
|
|
|
"""
|
|
|
|
|
2015-03-17 22:47:21 +00:00
|
|
|
|
2013-06-16 21:28:09 +00:00
|
|
|
class HMAC(hmac.HMAC):
|
2013-08-12 12:39:14 +00:00
|
|
|
"""Workaround a bug in Python < 3.4 Where HMAC does not accept memoryviews
|
|
|
|
"""
|
2013-06-16 21:28:09 +00:00
|
|
|
def update(self, msg):
|
|
|
|
self.inner.update(msg)
|
|
|
|
|
2012-12-04 22:02:10 +00:00
|
|
|
|
2013-06-20 10:44:58 +00:00
|
|
|
def key_creator(repository, args):
|
2013-08-10 11:02:20 +00:00
|
|
|
if args.encryption == 'keyfile':
|
2013-06-20 10:44:58 +00:00
|
|
|
return KeyfileKey.create(repository, args)
|
2013-08-10 11:02:20 +00:00
|
|
|
elif args.encryption == 'passphrase':
|
2013-06-20 10:44:58 +00:00
|
|
|
return PassphraseKey.create(repository, args)
|
2012-12-04 22:02:10 +00:00
|
|
|
else:
|
2013-06-20 10:44:58 +00:00
|
|
|
return PlaintextKey.create(repository, args)
|
2012-12-04 22:02:10 +00:00
|
|
|
|
|
|
|
|
2013-06-20 10:44:58 +00:00
|
|
|
def key_factory(repository, manifest_data):
|
2013-07-05 14:57:36 +00:00
|
|
|
if manifest_data[0] == KeyfileKey.TYPE:
|
2013-06-20 10:44:58 +00:00
|
|
|
return KeyfileKey.detect(repository, manifest_data)
|
2013-07-05 14:57:36 +00:00
|
|
|
elif manifest_data[0] == PassphraseKey.TYPE:
|
2013-06-20 10:44:58 +00:00
|
|
|
return PassphraseKey.detect(repository, manifest_data)
|
2013-07-05 14:57:36 +00:00
|
|
|
elif manifest_data[0] == PlaintextKey.TYPE:
|
2013-06-20 10:44:58 +00:00
|
|
|
return PlaintextKey.detect(repository, manifest_data)
|
2012-12-04 22:02:10 +00:00
|
|
|
else:
|
2014-12-17 20:55:33 +00:00
|
|
|
raise UnsupportedPayloadError(manifest_data[0])
|
2012-12-04 22:02:10 +00:00
|
|
|
|
2011-10-29 15:01:07 +00:00
|
|
|
|
2015-03-17 22:03:36 +00:00
|
|
|
class KeyBase:
|
2012-12-04 22:02:10 +00:00
|
|
|
|
2013-07-05 14:57:36 +00:00
|
|
|
def __init__(self):
|
|
|
|
self.TYPE_STR = bytes([self.TYPE])
|
|
|
|
|
2012-12-04 22:02:10 +00:00
|
|
|
def id_hash(self, data):
|
|
|
|
"""Return HMAC hash using the "id" HMAC key
|
|
|
|
"""
|
|
|
|
|
|
|
|
def encrypt(self, data):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def decrypt(self, id, data):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class PlaintextKey(KeyBase):
|
2013-07-05 14:57:36 +00:00
|
|
|
TYPE = 0x02
|
2012-12-04 22:02:10 +00:00
|
|
|
|
|
|
|
chunk_seed = 0
|
|
|
|
|
|
|
|
@classmethod
|
2013-06-20 10:44:58 +00:00
|
|
|
def create(cls, repository, args):
|
2013-08-10 11:02:20 +00:00
|
|
|
print('Encryption NOT enabled.\nUse the "--encryption=passphrase|keyfile" to enable encryption.')
|
2012-12-04 22:02:10 +00:00
|
|
|
return cls()
|
|
|
|
|
|
|
|
@classmethod
|
2013-06-20 10:44:58 +00:00
|
|
|
def detect(cls, repository, manifest_data):
|
2012-12-04 22:02:10 +00:00
|
|
|
return cls()
|
|
|
|
|
|
|
|
def id_hash(self, data):
|
2013-06-16 21:28:09 +00:00
|
|
|
return sha256(data).digest()
|
2012-12-04 22:02:10 +00:00
|
|
|
|
|
|
|
def encrypt(self, data):
|
2013-07-08 12:00:22 +00:00
|
|
|
return b''.join([self.TYPE_STR, zlib.compress(data)])
|
2012-12-04 22:02:10 +00:00
|
|
|
|
|
|
|
def decrypt(self, id, data):
|
2013-07-05 14:57:36 +00:00
|
|
|
if data[0] != self.TYPE:
|
2012-12-04 22:02:10 +00:00
|
|
|
raise IntegrityError('Invalid encryption envelope')
|
2013-07-08 12:00:22 +00:00
|
|
|
data = zlib.decompress(memoryview(data)[1:])
|
2013-06-16 21:28:09 +00:00
|
|
|
if id and sha256(data).digest() != id:
|
2012-12-04 22:02:10 +00:00
|
|
|
raise IntegrityError('Chunk id verification failed')
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
class AESKeyBase(KeyBase):
|
2013-08-12 12:39:14 +00:00
|
|
|
"""Common base class shared by KeyfileKey and PassphraseKey
|
|
|
|
|
|
|
|
Chunks are encrypted using 256bit AES in Counter Mode (CTR)
|
|
|
|
|
|
|
|
Payload layout: TYPE(1) + HMAC(32) + NONCE(8) + CIPHERTEXT
|
|
|
|
|
|
|
|
To reduce payload size only 8 bytes of the 16 bytes nonce is saved
|
|
|
|
in the payload, the first 8 bytes are always zeros. This does not
|
|
|
|
affect security but limits the maximum repository capacity to
|
|
|
|
only 295 exabytes!
|
|
|
|
"""
|
|
|
|
|
|
|
|
PAYLOAD_OVERHEAD = 1 + 32 + 8 # TYPE + HMAC + NONCE
|
2012-12-04 22:02:10 +00:00
|
|
|
|
|
|
|
def id_hash(self, data):
|
|
|
|
"""Return HMAC hash using the "id" HMAC key
|
|
|
|
"""
|
2013-06-16 21:28:09 +00:00
|
|
|
return HMAC(self.id_key, data, sha256).digest()
|
2012-12-04 22:02:10 +00:00
|
|
|
|
|
|
|
def encrypt(self, data):
|
|
|
|
data = zlib.compress(data)
|
2013-06-16 21:28:09 +00:00
|
|
|
self.enc_cipher.reset()
|
|
|
|
data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(data)))
|
2013-07-05 14:57:36 +00:00
|
|
|
hmac = HMAC(self.enc_hmac_key, data, sha256).digest()
|
|
|
|
return b''.join((self.TYPE_STR, hmac, data))
|
2012-12-04 22:02:10 +00:00
|
|
|
|
|
|
|
def decrypt(self, id, data):
|
2013-07-05 14:57:36 +00:00
|
|
|
if data[0] != self.TYPE:
|
2012-12-04 22:02:10 +00:00
|
|
|
raise IntegrityError('Invalid encryption envelope')
|
2013-07-05 14:57:36 +00:00
|
|
|
hmac = memoryview(data)[1:33]
|
|
|
|
if memoryview(HMAC(self.enc_hmac_key, memoryview(data)[33:], sha256).digest()) != hmac:
|
2012-12-04 22:02:10 +00:00
|
|
|
raise IntegrityError('Encryption envelope checksum mismatch')
|
2013-06-16 21:28:09 +00:00
|
|
|
self.dec_cipher.reset(iv=PREFIX + data[33:41])
|
|
|
|
data = zlib.decompress(self.dec_cipher.decrypt(data[41:])) # should use memoryview
|
|
|
|
if id and HMAC(self.id_key, data, sha256).digest() != id:
|
2012-12-04 22:02:10 +00:00
|
|
|
raise IntegrityError('Chunk id verification failed')
|
|
|
|
return data
|
|
|
|
|
2013-08-12 12:39:14 +00:00
|
|
|
def extract_nonce(self, payload):
|
2013-07-05 14:57:36 +00:00
|
|
|
if payload[0] != self.TYPE:
|
2012-12-18 20:58:58 +00:00
|
|
|
raise IntegrityError('Invalid encryption envelope')
|
|
|
|
nonce = bytes_to_long(payload[33:41])
|
|
|
|
return nonce
|
|
|
|
|
2012-12-04 22:02:10 +00:00
|
|
|
def init_from_random_data(self, data):
|
|
|
|
self.enc_key = data[0:32]
|
|
|
|
self.enc_hmac_key = data[32:64]
|
|
|
|
self.id_key = data[64:96]
|
2013-06-16 21:28:09 +00:00
|
|
|
self.chunk_seed = bytes_to_int(data[96:100])
|
2012-12-04 22:02:10 +00:00
|
|
|
# Convert to signed int32
|
|
|
|
if self.chunk_seed & 0x80000000:
|
|
|
|
self.chunk_seed = self.chunk_seed - 0xffffffff - 1
|
2013-06-16 21:28:09 +00:00
|
|
|
|
|
|
|
def init_ciphers(self, enc_iv=b''):
|
cleanup crypto.pyx, make it easier to adapt to other modes
There were some small issues:
a) it never called EVP_EncryptFinal_ex.
For CTR mode, this had no visible consequences as EVP_EncryptUpdate already yielded all ciphertext.
For cleanliness and to have correctness even in other modes, the missing call was added.
b) decrypt = encrypt hack
This is a nice hack to abbreviate, but it only works for modes without padding and without authentication.
For cleanliness and to have correctness even in other modes, the missing usage of the decrypt api was added.
c) outl == inl assumption
Again, True for CTR mode, but not for padding or authenticating modes.
Fixed so it computes the ciphertext / plaintext length based on api return values.
Other changes:
As encrypt and decrypt API calls are different even for initialization/reset, added a is_encrypt flag.
Defensive output buffer allocation. Added the length of one extra AES block (16bytes) so it would
work even with padding modes. 16bytes are needed because a full block of padding might get
added when the plaintext was a multiple of aes block size.
These changes are based on some experimental code I did for aes-cbc and aes-gcm.
While we likely won't ever want aes-cbc in attic (maybe gcm though?), I think it is cleaner
to not make too many mode specific assumptions and hacks, but just use the API as it
was meant to be used.
2015-03-03 18:19:28 +00:00
|
|
|
self.enc_cipher = AES(is_encrypt=True, key=self.enc_key, iv=enc_iv)
|
|
|
|
self.dec_cipher = AES(is_encrypt=False, key=self.enc_key)
|
2012-12-04 22:02:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
class PassphraseKey(AESKeyBase):
|
2013-07-05 14:57:36 +00:00
|
|
|
TYPE = 0x01
|
2013-06-22 21:08:53 +00:00
|
|
|
iterations = 100000
|
2012-12-04 22:02:10 +00:00
|
|
|
|
|
|
|
@classmethod
|
2013-06-20 10:44:58 +00:00
|
|
|
def create(cls, repository, args):
|
2012-12-04 22:02:10 +00:00
|
|
|
key = cls()
|
2013-07-08 21:38:27 +00:00
|
|
|
passphrase = os.environ.get('ATTIC_PASSPHRASE')
|
2012-12-04 22:02:10 +00:00
|
|
|
if passphrase is not None:
|
|
|
|
passphrase2 = passphrase
|
|
|
|
else:
|
|
|
|
passphrase, passphrase2 = 1, 2
|
|
|
|
while passphrase != passphrase2:
|
|
|
|
passphrase = getpass('Enter passphrase: ')
|
|
|
|
if not passphrase:
|
2013-06-03 11:45:48 +00:00
|
|
|
print('Passphrase must not be blank')
|
2012-12-04 22:02:10 +00:00
|
|
|
continue
|
|
|
|
passphrase2 = getpass('Enter same passphrase again: ')
|
|
|
|
if passphrase != passphrase2:
|
2013-06-03 11:45:48 +00:00
|
|
|
print('Passphrases do not match')
|
2013-06-20 10:44:58 +00:00
|
|
|
key.init(repository, passphrase)
|
2012-12-04 22:02:10 +00:00
|
|
|
if passphrase:
|
2013-06-03 11:45:48 +00:00
|
|
|
print('Remember your passphrase. Your data will be inaccessible without it.')
|
2012-12-04 22:02:10 +00:00
|
|
|
return key
|
|
|
|
|
|
|
|
@classmethod
|
2013-06-20 10:44:58 +00:00
|
|
|
def detect(cls, repository, manifest_data):
|
|
|
|
prompt = 'Enter passphrase for %s: ' % repository._location.orig
|
2012-12-04 22:02:10 +00:00
|
|
|
key = cls()
|
2013-07-08 21:38:27 +00:00
|
|
|
passphrase = os.environ.get('ATTIC_PASSPHRASE')
|
2012-12-04 22:02:10 +00:00
|
|
|
if passphrase is None:
|
|
|
|
passphrase = getpass(prompt)
|
|
|
|
while True:
|
2013-06-20 10:44:58 +00:00
|
|
|
key.init(repository, passphrase)
|
2012-12-04 22:02:10 +00:00
|
|
|
try:
|
|
|
|
key.decrypt(None, manifest_data)
|
2013-08-12 12:39:14 +00:00
|
|
|
num_blocks = num_aes_blocks(len(manifest_data) - 41)
|
|
|
|
key.init_ciphers(PREFIX + long_to_bytes(key.extract_nonce(manifest_data) + num_blocks))
|
2012-12-04 22:02:10 +00:00
|
|
|
return key
|
|
|
|
except IntegrityError:
|
|
|
|
passphrase = getpass(prompt)
|
|
|
|
|
2015-03-08 03:19:25 +00:00
|
|
|
def change_passphrase(self):
|
|
|
|
class ImmutablePassphraseError(Error):
|
|
|
|
"""The passphrase for this encryption key type can't be changed."""
|
|
|
|
|
|
|
|
raise ImmutablePassphraseError
|
|
|
|
|
2013-06-20 10:44:58 +00:00
|
|
|
def init(self, repository, passphrase):
|
|
|
|
self.init_from_random_data(pbkdf2_sha256(passphrase.encode('utf-8'), repository.id, self.iterations, 100))
|
2013-06-16 21:28:09 +00:00
|
|
|
self.init_ciphers()
|
2012-12-04 22:02:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
class KeyfileKey(AESKeyBase):
|
2013-07-08 21:38:27 +00:00
|
|
|
FILE_ID = 'ATTIC KEY'
|
2013-07-05 14:57:36 +00:00
|
|
|
TYPE = 0x00
|
2011-07-30 19:13:48 +00:00
|
|
|
|
2012-12-04 22:02:10 +00:00
|
|
|
@classmethod
|
2013-06-20 10:44:58 +00:00
|
|
|
def detect(cls, repository, manifest_data):
|
2012-12-04 22:02:10 +00:00
|
|
|
key = cls()
|
2013-06-20 10:44:58 +00:00
|
|
|
path = cls.find_key_file(repository)
|
2012-12-04 22:02:10 +00:00
|
|
|
prompt = 'Enter passphrase for key file %s: ' % path
|
2013-07-08 21:38:27 +00:00
|
|
|
passphrase = os.environ.get('ATTIC_PASSPHRASE', '')
|
2012-12-04 22:02:10 +00:00
|
|
|
while not key.load(path, passphrase):
|
|
|
|
passphrase = getpass(prompt)
|
2013-08-12 12:39:14 +00:00
|
|
|
num_blocks = num_aes_blocks(len(manifest_data) - 41)
|
|
|
|
key.init_ciphers(PREFIX + long_to_bytes(key.extract_nonce(manifest_data) + num_blocks))
|
2012-12-04 22:02:10 +00:00
|
|
|
return key
|
2011-07-30 19:13:48 +00:00
|
|
|
|
2012-12-04 22:02:10 +00:00
|
|
|
@classmethod
|
2013-06-20 10:44:58 +00:00
|
|
|
def find_key_file(cls, repository):
|
|
|
|
id = hexlify(repository.id).decode('ascii')
|
2011-08-06 11:01:58 +00:00
|
|
|
keys_dir = get_keys_dir()
|
2011-08-04 13:27:52 +00:00
|
|
|
for name in os.listdir(keys_dir):
|
|
|
|
filename = os.path.join(keys_dir, name)
|
2013-06-03 11:45:48 +00:00
|
|
|
with open(filename, 'r') as fd:
|
2011-08-04 13:27:52 +00:00
|
|
|
line = fd.readline().strip()
|
2013-07-08 21:38:27 +00:00
|
|
|
if line and line.startswith(cls.FILE_ID) and line[10:] == id:
|
2011-08-04 13:27:52 +00:00
|
|
|
return filename
|
2015-04-19 21:09:10 +00:00
|
|
|
raise KeyfileNotFoundError(repository._location.canonical_path(), get_keys_dir())
|
2011-08-04 13:27:52 +00:00
|
|
|
|
2012-12-04 22:02:10 +00:00
|
|
|
def load(self, filename, passphrase):
|
2013-06-03 11:45:48 +00:00
|
|
|
with open(filename, 'r') as fd:
|
|
|
|
cdata = a2b_base64(''.join(fd.readlines()[1:]).encode('ascii')) # .encode needed for Python 3.[0-2]
|
2012-12-04 22:02:10 +00:00
|
|
|
data = self.decrypt_key_file(cdata, passphrase)
|
|
|
|
if data:
|
|
|
|
key = msgpack.unpackb(data)
|
2013-06-03 11:45:48 +00:00
|
|
|
if key[b'version'] != 1:
|
2012-12-04 22:02:10 +00:00
|
|
|
raise IntegrityError('Invalid key file header')
|
2013-06-20 10:44:58 +00:00
|
|
|
self.repository_id = key[b'repository_id']
|
2013-06-03 11:45:48 +00:00
|
|
|
self.enc_key = key[b'enc_key']
|
|
|
|
self.enc_hmac_key = key[b'enc_hmac_key']
|
|
|
|
self.id_key = key[b'id_key']
|
|
|
|
self.chunk_seed = key[b'chunk_seed']
|
2012-12-04 22:02:10 +00:00
|
|
|
self.path = filename
|
|
|
|
return True
|
2011-09-04 21:02:47 +00:00
|
|
|
|
2012-12-04 22:02:10 +00:00
|
|
|
def decrypt_key_file(self, data, passphrase):
|
|
|
|
d = msgpack.unpackb(data)
|
2013-06-03 11:45:48 +00:00
|
|
|
assert d[b'version'] == 1
|
2013-06-20 11:42:10 +00:00
|
|
|
assert d[b'algorithm'] == b'sha256'
|
2013-06-16 21:28:09 +00:00
|
|
|
key = pbkdf2_sha256(passphrase.encode('utf-8'), d[b'salt'], d[b'iterations'], 32)
|
cleanup crypto.pyx, make it easier to adapt to other modes
There were some small issues:
a) it never called EVP_EncryptFinal_ex.
For CTR mode, this had no visible consequences as EVP_EncryptUpdate already yielded all ciphertext.
For cleanliness and to have correctness even in other modes, the missing call was added.
b) decrypt = encrypt hack
This is a nice hack to abbreviate, but it only works for modes without padding and without authentication.
For cleanliness and to have correctness even in other modes, the missing usage of the decrypt api was added.
c) outl == inl assumption
Again, True for CTR mode, but not for padding or authenticating modes.
Fixed so it computes the ciphertext / plaintext length based on api return values.
Other changes:
As encrypt and decrypt API calls are different even for initialization/reset, added a is_encrypt flag.
Defensive output buffer allocation. Added the length of one extra AES block (16bytes) so it would
work even with padding modes. 16bytes are needed because a full block of padding might get
added when the plaintext was a multiple of aes block size.
These changes are based on some experimental code I did for aes-cbc and aes-gcm.
While we likely won't ever want aes-cbc in attic (maybe gcm though?), I think it is cleaner
to not make too many mode specific assumptions and hacks, but just use the API as it
was meant to be used.
2015-03-03 18:19:28 +00:00
|
|
|
data = AES(is_encrypt=False, key=key).decrypt(d[b'data'])
|
2013-06-16 21:28:09 +00:00
|
|
|
if HMAC(key, data, sha256).digest() != d[b'hash']:
|
2012-12-04 22:02:10 +00:00
|
|
|
return None
|
|
|
|
return data
|
2011-07-30 19:13:48 +00:00
|
|
|
|
2012-12-04 22:02:10 +00:00
|
|
|
def encrypt_key_file(self, data, passphrase):
|
2011-07-30 19:13:48 +00:00
|
|
|
salt = get_random_bytes(32)
|
2013-06-20 11:42:10 +00:00
|
|
|
iterations = 100000
|
2013-06-16 21:28:09 +00:00
|
|
|
key = pbkdf2_sha256(passphrase.encode('utf-8'), salt, iterations, 32)
|
|
|
|
hash = HMAC(key, data, sha256).digest()
|
cleanup crypto.pyx, make it easier to adapt to other modes
There were some small issues:
a) it never called EVP_EncryptFinal_ex.
For CTR mode, this had no visible consequences as EVP_EncryptUpdate already yielded all ciphertext.
For cleanliness and to have correctness even in other modes, the missing call was added.
b) decrypt = encrypt hack
This is a nice hack to abbreviate, but it only works for modes without padding and without authentication.
For cleanliness and to have correctness even in other modes, the missing usage of the decrypt api was added.
c) outl == inl assumption
Again, True for CTR mode, but not for padding or authenticating modes.
Fixed so it computes the ciphertext / plaintext length based on api return values.
Other changes:
As encrypt and decrypt API calls are different even for initialization/reset, added a is_encrypt flag.
Defensive output buffer allocation. Added the length of one extra AES block (16bytes) so it would
work even with padding modes. 16bytes are needed because a full block of padding might get
added when the plaintext was a multiple of aes block size.
These changes are based on some experimental code I did for aes-cbc and aes-gcm.
While we likely won't ever want aes-cbc in attic (maybe gcm though?), I think it is cleaner
to not make too many mode specific assumptions and hacks, but just use the API as it
was meant to be used.
2015-03-03 18:19:28 +00:00
|
|
|
cdata = AES(is_encrypt=True, key=key).encrypt(data)
|
2011-07-30 19:13:48 +00:00
|
|
|
d = {
|
|
|
|
'version': 1,
|
|
|
|
'salt': salt,
|
|
|
|
'iterations': iterations,
|
2013-06-20 11:42:10 +00:00
|
|
|
'algorithm': 'sha256',
|
2011-07-30 19:13:48 +00:00
|
|
|
'hash': hash,
|
|
|
|
'data': cdata,
|
|
|
|
}
|
|
|
|
return msgpack.packb(d)
|
|
|
|
|
2012-12-04 22:02:10 +00:00
|
|
|
def save(self, path, passphrase):
|
2011-07-30 19:13:48 +00:00
|
|
|
key = {
|
|
|
|
'version': 1,
|
2013-06-20 10:44:58 +00:00
|
|
|
'repository_id': self.repository_id,
|
2011-07-30 19:13:48 +00:00
|
|
|
'enc_key': self.enc_key,
|
|
|
|
'enc_hmac_key': self.enc_hmac_key,
|
2012-12-18 20:58:58 +00:00
|
|
|
'id_key': self.id_key,
|
2011-07-30 19:13:48 +00:00
|
|
|
'chunk_seed': self.chunk_seed,
|
|
|
|
}
|
2012-12-04 22:02:10 +00:00
|
|
|
data = self.encrypt_key_file(msgpack.packb(key), passphrase)
|
2013-06-03 11:45:48 +00:00
|
|
|
with open(path, 'w') as fd:
|
2013-06-20 10:44:58 +00:00
|
|
|
fd.write('%s %s\n' % (self.FILE_ID, hexlify(self.repository_id).decode('ascii')))
|
2013-06-20 11:42:10 +00:00
|
|
|
fd.write('\n'.join(textwrap.wrap(b2a_base64(data).decode('ascii'))))
|
2014-03-28 21:56:40 +00:00
|
|
|
fd.write('\n')
|
2011-10-27 20:17:47 +00:00
|
|
|
self.path = path
|
2011-07-30 19:13:48 +00:00
|
|
|
|
2012-12-04 22:02:10 +00:00
|
|
|
def change_passphrase(self):
|
|
|
|
passphrase, passphrase2 = 1, 2
|
|
|
|
while passphrase != passphrase2:
|
|
|
|
passphrase = getpass('New passphrase: ')
|
|
|
|
passphrase2 = getpass('Enter same passphrase again: ')
|
|
|
|
if passphrase != passphrase2:
|
2013-06-03 11:45:48 +00:00
|
|
|
print('Passphrases do not match')
|
2012-12-04 22:02:10 +00:00
|
|
|
self.save(self.path, passphrase)
|
2013-06-03 11:45:48 +00:00
|
|
|
print('Key file "%s" updated' % self.path)
|
2012-12-04 22:02:10 +00:00
|
|
|
|
|
|
|
@classmethod
|
2013-06-20 10:44:58 +00:00
|
|
|
def create(cls, repository, args):
|
|
|
|
filename = args.repository.to_key_filename()
|
2011-08-04 13:27:52 +00:00
|
|
|
path = filename
|
2012-12-04 22:02:10 +00:00
|
|
|
i = 1
|
2011-08-04 13:27:52 +00:00
|
|
|
while os.path.exists(path):
|
|
|
|
i += 1
|
|
|
|
path = filename + '.%d' % i
|
2013-07-08 21:38:27 +00:00
|
|
|
passphrase = os.environ.get('ATTIC_PASSPHRASE')
|
2012-12-04 22:02:10 +00:00
|
|
|
if passphrase is not None:
|
|
|
|
passphrase2 = passphrase
|
2011-08-06 11:01:58 +00:00
|
|
|
else:
|
2012-12-04 22:02:10 +00:00
|
|
|
passphrase, passphrase2 = 1, 2
|
|
|
|
while passphrase != passphrase2:
|
|
|
|
passphrase = getpass('Enter passphrase (empty for no passphrase):')
|
|
|
|
passphrase2 = getpass('Enter same passphrase again: ')
|
|
|
|
if passphrase != passphrase2:
|
2013-06-03 11:45:48 +00:00
|
|
|
print('Passphrases do not match')
|
2012-12-04 22:02:10 +00:00
|
|
|
key = cls()
|
2013-06-20 10:44:58 +00:00
|
|
|
key.repository_id = repository.id
|
2012-12-04 22:02:10 +00:00
|
|
|
key.init_from_random_data(get_random_bytes(100))
|
2013-06-16 21:28:09 +00:00
|
|
|
key.init_ciphers()
|
2012-12-04 22:02:10 +00:00
|
|
|
key.save(path, passphrase)
|
2013-06-03 11:45:48 +00:00
|
|
|
print('Key file "%s" created.' % key.path)
|
|
|
|
print('Keep this file safe. Your data will be inaccessible without it.')
|
2012-12-04 22:02:10 +00:00
|
|
|
return key
|