diff --git a/example.yaml b/example.yaml index a911a3c..7279c42 100644 --- a/example.yaml +++ b/example.yaml @@ -6,6 +6,7 @@ compress_method: "bzip2" # Optional. Default value: null. Supported value: "bzi # Optional. Default value: null. bzip2 support 1-9 (Default: 9). gzip support 0-9 (Default: 9). lzma or lzip support 0-9 (Default: 6). # zstd support 0-22 (Default: 3). brotli support 0-11 (Default: unset). compress_level: 6 +encrypt_db: false # Optional. Default value: false. Encrypt the database. Warning: The default python sqlite library don't support encrypt, it just ignore encrypt phases. programs: - name: Your program name # This name is used to identify different application. base: /path/to/save/path # Must be absoulte path. diff --git a/game_backuper/cml.py b/game_backuper/cml.py index 0dd7d8f..eb3cdb0 100644 --- a/game_backuper/cml.py +++ b/game_backuper/cml.py @@ -37,10 +37,12 @@ class Opts: action = OptAction.BACKUP programs_list = None optimize_db = False + change_key = False def __init__(self, cml: List[str]): try: - r = getopt(cml, 'hc:', ['help', 'config=', 'optimize-db']) + r = getopt(cml, 'hc:', ['help', 'config=', 'optimize-db', + 'change-key']) for i in r[0]: if i[0] == '-h' or i[0] == '--help': self.print_help() @@ -50,6 +52,8 @@ class Opts: self.config_file = i[1] elif i[0] == '--optimize-db': self.optimize_db = True + elif i[0] == '--change-key': + self.change_key = True if len(r[1]) > 0: cm = r[1] re = OptAction.from_str(cm[0]) @@ -78,4 +82,5 @@ game-backuper [options] list_leveldb_key [ [...]] Options: -h, --help Print help message. -c, --config Set config file. - --optimize-db Optimize the sqlite3 database''') + --optimize-db Optimize the sqlite3 database + --change-key Change encrypt password''') diff --git a/game_backuper/config.py b/game_backuper/config.py index 3db9df9..cb3cde0 100644 --- a/game_backuper/config.py +++ b/game_backuper/config.py @@ -418,6 +418,7 @@ class Program(BasicOption, NFBasicOption): class Config(BasicOption, NFBasicOption): dest = '' + encrypt_db = False progs = [] progs_name = [] @@ -431,6 +432,12 @@ class Config(BasicOption, NFBasicOption): if 'dest' not in t or not isinstance(t['dest'], str): raise ValueError("Config file don't have dest or dest is not str.") self.dest = t['dest'] + if 'encrypt_db' in t: + if not isinstance(t['encrypt_db'], bool): + raise ValueError('encrypt_db should be true or false.') + self.encrypt_db = t['encrypt_db'] + else: + self.encrypt_db = False if 'programs' not in t: raise ValueError("No programs found.") self.parse_all(t) diff --git a/game_backuper/db.py b/game_backuper/db.py index 37a6941..068f616 100644 --- a/game_backuper/db.py +++ b/game_backuper/db.py @@ -1,7 +1,13 @@ -from sqlite3 import connect +from getpass import getpass +from os import close from os.path import join -from typing import List, Union +from shutil import move +from sqlite3 import connect, Connection, DatabaseError +from tempfile import mkstemp from threading import Lock +from typing import List, Union +from game_backuper.cml import Opts +from game_backuper.config import Config from game_backuper.file import File, hydrate_file_if_needed from game_backuper.filetype import FileType @@ -56,17 +62,64 @@ class Db: self.db.execute(FILETYPE_TABLE) self.db.commit() - def __init__(self, loc: str, optimize_db: bool = False): - fn = join(loc, "data.db") + def __init__(self, config: Config, opts: Opts): + self._cfg = config + self._opt = opts + fn = join(config.dest, "data.db") hydrate_file_if_needed(fn) self.db = connect(fn, check_same_thread=False) - if optimize_db: + if config.encrypt_db: + passpharse = getpass('Please input the password of the database:') + if not self.encrypted: + tfn = mkstemp() + close(tfn[0]) + tfn = tfn[1] + db = connect(tfn) + self.__set_encrypt_key(passpharse, db) + for q in self.db.iterdump(): + db.execute(q) + self.db.close() + db.close() + move(tfn, fn) + self.db = connect(fn, check_same_thread=False) + elif opts.change_key: + self.__set_encrypt_key(passpharse) + passpharse = getpass('Please input new password of the database:') # noqa: E501 + tfn = mkstemp() + close(tfn[0]) + tfn = tfn[1] + db = connect(tfn) + self.__set_encrypt_key(passpharse, db) + for q in self.db.iterdump(): + db.execute(q) + self.db.close() + db.close() + move(tfn, fn) + self.db = connect(fn, check_same_thread=False) + self.__set_encrypt_key(passpharse) + else: + if self.encrypted: + passpharse = getpass('Please input the password of the database:') # noqa: E501 + self.__set_encrypt_key(passpharse) + tfn = mkstemp() + close(tfn[0]) + tfn = tfn[1] + db = connect(tfn) + for q in self.db.iterdump(): + db.execute(q) + self.db.close() + db.close() + move(tfn, fn) + self.db = connect(fn, check_same_thread=False) + if opts.optimize_db: self.db.execute('VACUUM;') self.db.commit() ok = self.__check_database() if not ok: self.__create_table() self._lock = Lock() + if config.encrypt_db and not self.encrypted: + print('Warning: Current library do not support encryption.') def __read_version(self) -> List[int]: if 'version' not in self._exist_table: @@ -75,6 +128,12 @@ class Db: for i in cur: return [k for k in i if isinstance(k, int)] + def __set_encrypt_key(self, key: str, db: Connection = None): + if db is None: + db = self.db + db.execute('PRAGMA cipher_salt = "x\'2d506b1d2c3e7b075518f9db81039657\'";') # noqa: E501 + db.execute('PRAGMA key = \'%s\';' % (key.replace("'", "\\'"))) + def __updateExistsTable(self): cur = self.db.execute('SELECT * FROM main.sqlite_master;') self._exist_table = {} @@ -105,6 +164,17 @@ class Db: (i[0], f.type)) self.db.commit() + @property + def encrypted(self): + try: + con = connect(join(self._cfg.dest, 'data.db')) + con.execute('SELECT count(*) FROM sqlite_master;') + con.close() + return False + except DatabaseError: + con.close() + return True + def get_file(self, prog: str, file: str) -> File: with self._lock: cur = self.db.execute( diff --git a/game_backuper/file.py b/game_backuper/file.py index d810fdd..06afd30 100644 --- a/game_backuper/file.py +++ b/game_backuper/file.py @@ -6,8 +6,11 @@ from shutil import copy2 from game_backuper.filetype import FileType from platform import system if system() == "Windows": - from game_backuper.cfapi import hydrate_file - have_cfapi = True + try: + from game_backuper.cfapi import hydrate_file + have_cfapi = True + except Exception: + have_cfapi = False else: have_cfapi = False diff --git a/game_backuper/main.py b/game_backuper/main.py index d44b83e..906a1ff 100644 --- a/game_backuper/main.py +++ b/game_backuper/main.py @@ -14,6 +14,6 @@ def main(cm=None): cfg = Config(cml.config_file) if not exists(cfg.dest): makedirs(cfg.dest) - db = Db(cfg.dest, cml.optimize_db) + db = Db(cfg, cml) bk = Backuper(db, cfg, cml) return bk.run()