PK!ȅ8XXlp_backup/__init__.pyfrom .runner import Runner # __docurl__ = "https://lastpass-local-backup.readthedocs.io"PK!/Dlp_backup/exceptions.pyclass LoginFailed(Exception): pass class ConfigurationError(Exception): pass class InvalidKey(Exception): pass class BackupFailed(Exception): pass class NoWebdav(Exception): pass PK!*|"X X lp_backup/file_io.pyimport botocore import fs from fs.copy import copy_file from fs.errors import DirectoryExists from fs import tempfs from lp_backup import exceptions def write_out_backup(backing_store_fs, data, outfile, prefix=''): """ Write the backup data to its final location. A backing store is required and either a filepath to the packaged backup or the tmp filesystem is required. :param backing_store_fs: a pyfilesystem2 object to be the final storage location of the backup. (should be `OSFS`, `S3FS`, `FTPFS`, etc.) Can be a single object or list of filesystem objects for copying to multiple backing stores. :param data: the byte stream that needs to be written to the file on the backing store fs. :param outfile: the name of the file to write out to. :param optional prefix: a parent directory for the files to be saved under. This is can be a good place to encode some information about the backup. A slash will be appended to the prefix to create a directory or pseudo-directory structure. """ if prefix and not prefix[-1] == '/': prefix = prefix + '/' if not isinstance(backing_store_fs, list): backing_store_fs = [backing_store_fs] for backing_fs in backing_store_fs: # print(backing_fs) tmp = tempfs.TempFS() with tmp.open("lp-tmp-backup", 'wb') as tmp_file: tmp_file.write(data) try: backing_fs.makedirs(prefix) except DirectoryExists: pass # print(prefix, outfile) copy_file(tmp, "lp-tmp-backup", backing_fs, str(prefix + outfile)) tmp.clean() def read_backup(backing_store_fs, infile, prefix=""): """ Read a backup file from some pyfilesystem. :param backing_store_fs: The pyfilesystem object where the file is located :param infile: the name of the file :param optional prefix: the prefix before the filename :return: raw file data """ tmp = tempfs.TempFS() # data = "" if prefix and not prefix[-1] == '/': prefix = prefix + '/' if not isinstance(backing_store_fs, list): backing_store_fs = [backing_store_fs] restore_succeeded = False for backing_fs in backing_store_fs: try: copy_file(backing_fs, prefix + infile, tmp, infile) restore_succeeded = True break except (botocore.exceptions.NoCredentialsError, OSError, fs.errors.ResourceNotFound, fs.errors.PermissionDenied): continue if not restore_succeeded: raise exceptions.ConfigurationError("Specified file could not be found in any" " of the available backing stores.") with tmp.open(infile, 'rb') as retrieved_file: data = retrieved_file.read() tmp.clean() return data PK!m8lp_backup/interface.pyimport click import os from pathlib import Path from lp_backup import Runner USER_HOME = os.path.expanduser('~') @click.group() @click.option('-c', "--config", help="Path to config file", default=f"{USER_HOME}/.config/lp_backup.yml") @click.pass_context def cli(ctx, config): ctx.obj = dict() ctx.obj["CONFIG"] = Path(config) @cli.command(help="Backup lastpass based onf config file") @click.pass_context def backup(ctx): runner = Runner(ctx.obj["CONFIG"]) backup_file_name = runner.backup() print(f"New backup is {backup_file_name}") @cli.command(help="Restore csv file based on config file.") @click.pass_context def restore(ctx): runner = Runner(ctx.obj["CONFIG"]) restore_file_path = runner.restore() print(f"Restored file is {restore_file_path}. It is NOT encrypted, " f"be sure to keep it safe and delete it when not needed.") PK!hhlp_backup/runner.pyimport datetime import os import lzma import subprocess from ruamel.yaml import YAML from cryptography.fernet import Fernet import fs from fs.errors import CreateFailed from fs_s3fs import S3FS from lp_backup import file_io from lp_backup import exceptions webdav_available = False try: from webdavfs.webdavfs import WebDAVFS webdav_available = True except ModuleNotFoundError: webdav_available = False class Runner(object): """ This class handles orchestration of downloading and storing the backup. Options are set in a yaml configuration file. There is an :download:`example ` you can use as a starting point. :param path: absolute path to the file on the system or relative to the FS object supplied in the filesystem parameter :param keyword filesystem: a pyfilesystem2 FS object where the yaml config file is located. """ def __init__(self, path, *, filesystem=None): self.yaml = YAML() self.config_path = str(path) if not filesystem: self.filesystem = fs.open_fs('/') else: self.filesystem = filesystem with self.filesystem.open(self.config_path, 'r') as configfile: self.config = self.yaml.load(configfile) # self.sultan = Sultan() self.logged_in = False self.configure_encryption() def login(self): trust = "" if self.config['Trust']: trust = "--trust" out = subprocess.run(["lpass", "login", self.config["Email"], trust], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if out.stderr: print(out.stderr) raise exceptions.LoginFailed(out.stderr) if "Success:" in out.stdout.decode('utf-8'): self.logged_in = True else: print(out.stderr + " " + out.stdout) raise exceptions.LoginFailed(out.stderr + " " + out.stdout) def configure_encryption(self): if self.config["Encryption Key"] is None: self.fernet = None return if self.config["Encryption Key"].lower() == "generate": new_key = Fernet.generate_key() self.config["Encryption Key"] = new_key with self.filesystem.open(self.config_path, 'w') as config_file: self.yaml.dump(self.config, config_file) try: self.fernet = Fernet(self.config["Encryption Key"]) except ValueError as err: raise exceptions.InvalidKey("Could not find valid encryption key: " + err) def backup(self): """ Using the configuration from the file, create the backup. """ if not self.logged_in: self.login() run_backup = subprocess.run(["lpass", "export"], stderr=subprocess.PIPE, stdout=subprocess.PIPE) file_suffix = '.csv' if run_backup.stderr: raise exceptions.BackupFailed(run_backup.stderr) # print("backup downloaded") backup_data = run_backup.stdout # backup_data = '\n'.join(backup_lines) if self.fernet: backup_data = self.fernet.encrypt(backup_data) file_suffix += ".encrypted" if self.config.get("Compression", False): backup_data = lzma.compress(backup_data) file_suffix += ".xz" outfs = None prefix = None try: outfs = self._configure_backing_store() prefix = self.config.get('Prefix', '') if self.config.get('Date', False): date = datetime.datetime.today().isoformat() + "-" else: date = "" except KeyError as err: _config_error(err) outfile = (date + self.config["Email"] + "-lastpass-backup" + file_suffix) file_io.write_out_backup( backing_store_fs=outfs, outfile=outfile, prefix=prefix, data=backup_data ) return outfile def restore(self, infilename, new_file): """ Restore backup to a plain text csv file for uploading to password manager. :param infilename: the name of the backup file :param new_file: the filename to save the data to """ try: restorefs = self._configure_backing_store() prefix = self.config.get("Prefix", "") except KeyError as err: _config_error(err) restored_data = file_io.read_backup(restorefs, infilename, prefix) if self.config.get("Compression", False): restored_data = lzma.decompress(restored_data) if self.fernet: restored_data = self.fernet.decrypt(restored_data) with self.filesystem.open(str(new_file), 'w') as the_new_file: the_new_file.write(restored_data.decode('utf-8')) return new_file def _configure_backing_store(self): try: backing_stores = [] for bs in self.config['Backing Store']: if 'Type' in bs: for key, item in bs.items(): bs[key] = _get_from_env(item) if bs['Type'].lower() == 's3': backing_stores.append(S3FS( bs['Bucket'], strict=False, aws_access_key_id=bs.get('Key ID', None), aws_secret_access_key=bs.get('Secret Key', None), endpoint_url=bs.get('Endpoint URL', None) )) elif 'dav' in bs['Type'].lower(): if not webdav_available: raise exceptions.NoWebdav("no webdavfs module was found") if bs['Root'][0] != '/': bs['Root'] = '/' + bs['Root'] backing_stores.append(WebDAVFS( url=bs['Base URL'], login=bs['Username'], password=bs['Password'], root=bs['Root'] )) else: _config_error("Unknown filesystem type.") else: backing_stores.append(fs.open_fs(bs['URI'], create=True)) except (KeyError, OSError, CreateFailed) as err: _config_error(err) return backing_stores def _config_error(err=''): raise exceptions.ConfigurationError( "Options are missing in the configuration file. " f"Pleaseconsult the docs at https://lastpass-local-backup.readthedocs.io\n" f"{err}") def _get_from_env(item): if item is None: return None try: if item[0] == '$': return os.environ[item[1:]] except TypeError: pass return item PK!o7++"lp_backup-0.1.10.dist-info/LICENSEMIT License Copyright (c) 2018 Rick Henry Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HnHTU lp_backup-0.1.10.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!HO#lp_backup-0.1.10.dist-info/METADATATo6ο Xdig&@N`'ۊ`HhHʩ-jX?0yw;gXK~ pHGdɒeO%hHR5cXdN@8 ɘCጂ+eޓOFIJfή+qס2n s=']]%\1!sD43ߵp'7 #z28=CQ"(Gc^LZgV5L0Oǩ/_gKtVMbrߵzaZPU"=ςOe?Y/I[%OVM$nIINl!y!n#!zظ}&IB |qcJq@puTLRhN1R^}qxPLGJpu"2;Kl(q|E~+VcAe߽rs4MR=5.385=iabkn>]ص+SV衽IW;H+_wyt!Wn<#ї-7|׵oWE)cӰZ^Ĩp=2 e䏚Hk{- ţT*xӳ4,Xv3B*G58PK!ȅ8XXlp_backup/__init__.pyPK!/Dlp_backup/exceptions.pyPK!*|"X X lp_backup/file_io.pyPK!m8 lp_backup/interface.pyPK!hhlp_backup/runner.pyPK!o7++"g,lp_backup-0.1.10.dist-info/LICENSEPK!HnHTU 0lp_backup-0.1.10.dist-info/WHEELPK!HO#d1lp_backup-0.1.10.dist-info/METADATAPK!H$G!4lp_backup-0.1.10.dist-info/RECORDPK 6