[dev] backup authentication tool source
tool to create an archive of all authentication files on one's computer and encrypt it in memory with AES-256-CBC
This commit is contained in:
commit
efda3dd3d8
|
@ -0,0 +1,2 @@
|
|||
**/venv*
|
||||
**/*.sw*
|
|
@ -0,0 +1,8 @@
|
|||
astroid==2.3.3
|
||||
isort==4.3.21
|
||||
lazy-object-proxy==1.4.3
|
||||
mccabe==0.6.1
|
||||
pycrypto==2.6.1
|
||||
pylint==2.4.4
|
||||
six==1.14.0
|
||||
wrapt==1.11.2
|
|
@ -0,0 +1,260 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
backup and encrypt local authentication files
|
||||
Python >= 3.8 required
|
||||
"""
|
||||
|
||||
import os
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
import argparse as ap
|
||||
import pathlib as pl
|
||||
import tarfile as tf
|
||||
import getpass
|
||||
import base64
|
||||
import hashlib as hl
|
||||
|
||||
from Crypto import Random
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
################################################################################
|
||||
|
||||
# Crypto Python >= 3.3 compat https://bugs.python.org/issue14309
|
||||
time.clock = time.time
|
||||
|
||||
HOME = os.path.expanduser('~')
|
||||
filenames = [
|
||||
pl.Path(HOME, filename) for filename in (
|
||||
".ssh/config",
|
||||
)
|
||||
]
|
||||
dirnames = [
|
||||
pl.Path(HOME, dirent) for dirent in (
|
||||
".ssh",
|
||||
".gnupg",
|
||||
".password-store",
|
||||
)
|
||||
]
|
||||
RES_FILE = pl.Path(HOME, "backup_authentication.tar.gz.enc")
|
||||
TARFILE = pl.Path(HOME, "backup_authentication.tar.gz")
|
||||
DESCRIPTION = \
|
||||
r"""backup_authentication creates an encrypted backup of standard authentication
|
||||
files for *nix systems, e.g. .ssh/, .password-store/, .gnupg/.
|
||||
"""
|
||||
MANUAL = \
|
||||
"""backup_authentication encrypts data in memory, first creating a tarball
|
||||
archive and then using AES-256-CBC to encrypt it. It requires Python3.8+ to
|
||||
run. You can customize the files and directories to backup and encrypt in
|
||||
the configuration above with the variables <filenames> and <dirnames>.
|
||||
"""
|
||||
|
||||
################################################################################
|
||||
|
||||
class Singleton(type):
|
||||
"""this is a metaclass. Class definition are defined with
|
||||
metaclasses.
|
||||
This class enforces uniqueness of instances of a class whose
|
||||
metaclass is this Singleton. In other words, if a class inherits
|
||||
from this class, then all instances objects of this class are the
|
||||
same, i.e. point to the same value in memory
|
||||
"""
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
# delete singleton. e.g. ClassName(delete_singleton=True)
|
||||
if kwargs.get("delete_singleton", None):
|
||||
cls._instances = {}
|
||||
return None
|
||||
# create singleton instance if not exists. return instance.
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
class Logger(metaclass=Singleton):
|
||||
"""module logger
|
||||
"""
|
||||
@staticmethod
|
||||
def log(msg, *args, **kwargs):
|
||||
"""log everything
|
||||
"""
|
||||
print(msg, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def display(cls, msg, *args, **kwargs):
|
||||
cls.log(msg, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def error(cls, msg, *args, **kwargs):
|
||||
cls.log(msg, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def warn(cls, msg, *args, **kwargs):
|
||||
cls.log(msg, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
class AESCipher:
|
||||
"""modified from
|
||||
https://stackoverflow.com/questions/12524994/encrypt-decrypt-using-pycrypto-aes-256
|
||||
"""
|
||||
def __init__(self, key: str):
|
||||
"""initialize an object which can encrypt or decrypt bytes with
|
||||
AES-256-CBC cypher
|
||||
"""
|
||||
self.bs = AES.block_size
|
||||
self.key = hl.sha256(key.encode()).digest()
|
||||
|
||||
def encrypt(self, raw: bytes) -> bytes:
|
||||
"""encrypt bytes with AES-256-CBC cypher
|
||||
|
||||
"""
|
||||
raw = self._pad(raw)
|
||||
iv = Random.new().read(AES.block_size)
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, iv)
|
||||
return base64.b64encode(iv + cipher.encrypt(raw))
|
||||
|
||||
def decrypt(self, enc: bytes):
|
||||
"""decrypt bytes with AES-256-CBC cypher
|
||||
"""
|
||||
enc = base64.b64decode(enc)
|
||||
iv = enc[:AES.block_size]
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, iv)
|
||||
return self._unpad(cipher.decrypt(enc[AES.block_size:]))
|
||||
|
||||
def _pad(self, s):
|
||||
"""helper function used to pad data to a block for AES.
|
||||
"""
|
||||
return s + ((self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)).encode()
|
||||
|
||||
@staticmethod
|
||||
def _unpad(s):
|
||||
"""helper function used to unpad encrypted data in the last AES block
|
||||
"""
|
||||
return s[:-ord(s[len(s)-1:])]
|
||||
|
||||
################################################################################
|
||||
|
||||
def exit_error(msg):
|
||||
"""exit program with an error
|
||||
"""
|
||||
if msg:
|
||||
LOGGER.error(msg)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def password_prompt(retry: bool = False) -> str:
|
||||
"""prompt user for password and verification. Check equality
|
||||
"""
|
||||
while len(s := {getpass.getpass("password: "), getpass.getpass("retype password: ")}) != 1:
|
||||
if len(s) == 1:
|
||||
break
|
||||
if not retry:
|
||||
return exit_error("ERROR: passwords do NOT match")
|
||||
LOGGER.warn("passwords do NOT match")
|
||||
return next(iter(s))
|
||||
|
||||
|
||||
def find_all_matching_dirs_and_files(dirnames: list, filenames: list) -> list:
|
||||
"""
|
||||
"""
|
||||
dirs_and_files = set()
|
||||
for dirent in dirnames:
|
||||
for root, dirs, files in os.walk(dirent, followlinks=False):
|
||||
for dirname in dirs:
|
||||
dirs_and_files.add(pl.Path(root, dirname))
|
||||
for filename in files:
|
||||
dirs_and_files.add(pl.Path(root, filename))
|
||||
|
||||
for filename in filenames:
|
||||
dirs_and_files.add(pl.Path(filename))
|
||||
bad_paths = tuple(entry for entry in dirs_and_files if not os.path.exists(entry))
|
||||
assert not bad_paths, f"Invalid path / files: {bad_paths}"
|
||||
LOGGER.log(f"Found {len(dirs_and_files)} directories and files to backup.")
|
||||
return dirs_and_files
|
||||
|
||||
|
||||
def create_tar_archive(password: str, dirs_and_files: set):
|
||||
"""
|
||||
"""
|
||||
buf = io.BytesIO()
|
||||
with tf.open(name=None, mode="w:gz", fileobj=buf) as archive:
|
||||
for entry in dirs_and_files:
|
||||
archive.add(entry)
|
||||
buf.seek(0)
|
||||
aes_tool = AESCipher(password)
|
||||
encrypted_data = aes_tool.encrypt(buf.read())
|
||||
with open(RES_FILE, mode="wb") as fwb:
|
||||
fwb.write(encrypted_data)
|
||||
LOGGER.log("OK: archive created")
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""parse program arguments
|
||||
"""
|
||||
parser = ap.ArgumentParser(description=DESCRIPTION)
|
||||
parser.add_argument("-e",
|
||||
"--encrypt",
|
||||
help="create an encrypted backup of files",
|
||||
action="store_true",
|
||||
dest="ACTION",
|
||||
default=True
|
||||
)
|
||||
parser.add_argument("-d",
|
||||
"--decrypt",
|
||||
help="decrypt AES archive",
|
||||
action="store_false",
|
||||
dest="ACTION",
|
||||
default=True,
|
||||
)
|
||||
parser.add_argument("-m",
|
||||
"--man",
|
||||
help="advanced manual for backup_authentication",
|
||||
action="store_true",
|
||||
dest="MANUAL",
|
||||
default=False,
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
"""main function for encryption
|
||||
"""
|
||||
password = password_prompt(retry=False)
|
||||
dirs_and_files = find_all_matching_dirs_and_files(dirnames, filenames)
|
||||
create_tar_archive(password, dirs_and_files)
|
||||
|
||||
|
||||
def decrypt():
|
||||
"""reverse encryption, write tar.gz backup file
|
||||
"""
|
||||
with open(RES_FILE, mode='rb') as frb:
|
||||
aes_tool = AESCipher(getpass.getpass("password: "))
|
||||
data = aes_tool.decrypt(frb.read())
|
||||
with open(TARFILE, mode="wb") as fwb:
|
||||
fwb.write(data)
|
||||
LOGGER.log("OK: archive decrypted")
|
||||
|
||||
################################################################################
|
||||
|
||||
LOGGER = Logger()
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = parse_args()
|
||||
args = parser.parse_args()
|
||||
if args.MANUAL:
|
||||
LOGGER.log(MANUAL)
|
||||
sys.exit(0)
|
||||
action = args.ACTION
|
||||
if action is True:
|
||||
main()
|
||||
elif action is False:
|
||||
decrypt()
|
||||
else:
|
||||
LOGGER.display(f"Nothing to do: {ACTION}")
|
||||
|
||||
################################################################################
|
Loading…
Reference in New Issue