mirror of
				https://github.com/netinvent/npbackup.git
				synced 2025-10-31 07:47:02 +08:00 
			
		
		
		
	Implement --create-key and fix alternative key usage, closes #56
This commit is contained in:
		
							parent
							
								
									dd13d9520e
								
							
						
					
					
						commit
						748bafb2b4
					
				
					 8 changed files with 69 additions and 30 deletions
				
			
		|  | @ -1,4 +1,5 @@ | |||
| This folder may contain private overrides for NPBackup secrets. | ||||
| This folder may contain overrides for NPBackup secrets. | ||||
| If these files exist, NPBackup will be compiled as "private build". | ||||
| 
 | ||||
| Overrides are used by default if found at execution time. | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										19
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								README.md
									
										
									
									
									
								
							|  | @ -199,10 +199,25 @@ While admin user experience is important, NPBackup also offers a GUI for end use | |||
| 
 | ||||
| ## Security | ||||
| 
 | ||||
| NPBackup inherits all security measures of it's backup backend (currently restic with AES-256 client side encryption including metadata), append only mode REST server backend. | ||||
| NPBackup inherits all security measures of it's backup backend (currently restic with AES-256 client side encryption including metadata) and all security options from it's storage backends. | ||||
| 
 | ||||
| On top of those, NPBackup itself encrypts sensible information like the repo uri and password, as well as the metrics http username and password.   | ||||
| This ensures that end users can restore data without the need to know any password, without compromising a secret. Note that in order to use this function, one needs to use the compiled version of NPBackup, so AES-256 keys are never exposed. Internally, NPBackup never directly uses the AES-256 key, so even a memory dump won't be enough to get the key. | ||||
| This ensures that end users can backup/restore data without the need to know any password, avoiding secret compromission.   | ||||
| Note that NPBackup uses an AES-256 key itself, in order to encrypt sensible data. The public (git) version of NPBackup uses the default encryption key that comes with the official NPBackup repo. | ||||
| 
 | ||||
| You can generate a new AES-256 key with `npbackup-cli --create-key npbackup.key` and use it via an environment variable: | ||||
| 
 | ||||
| Use a file | ||||
| ``` | ||||
| export NPBACKUP_KEY_LOCATION=/path/to/npbackup.key | ||||
| ``` | ||||
| 
 | ||||
| Use a command that provides the key as it's output | ||||
| ``` | ||||
| export NPBACKUP_KEY_COMMAND=my_key_command | ||||
| ``` | ||||
| 
 | ||||
| You may also compile your own NPBackup executables that directly contain the AES-256 key. See instructions in PRIVATE directory to setup keys. | ||||
| 
 | ||||
| ## Permission restriction system | ||||
| 
 | ||||
|  |  | |||
|  | @ -26,7 +26,6 @@ from npbackup.__debug__ import _DEBUG | |||
| from npbackup.common import execution_logs | ||||
| from npbackup.core import upgrade_runner | ||||
| from npbackup.core.i18n_helper import _t | ||||
| from npbackup import key_management | ||||
| 
 | ||||
| if os.name == "nt": | ||||
|     from npbackup.windows.task import create_scheduled_task | ||||
|  | @ -253,6 +252,13 @@ This is free software, and you are welcome to redistribute it under certain cond | |||
|         required=False, | ||||
|         help="Launch an operation on a group of repositories given by --repo-group", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--create-key", | ||||
|         type=str, | ||||
|         default=False, | ||||
|         required=False, | ||||
|         help="Create a new encryption key, requires a file path", | ||||
|     ) | ||||
|     args = parser.parse_args() | ||||
| 
 | ||||
|     if args.log_file: | ||||
|  | @ -283,6 +289,13 @@ This is free software, and you are welcome to redistribute it under certain cond | |||
|             print(LICENSE_TEXT) | ||||
|         sys.exit(0) | ||||
| 
 | ||||
|     if args.create_key: | ||||
|         result = key_management.create_key_file(args.create_key) | ||||
|         if result: | ||||
|             sys.exit(0) | ||||
|         else: | ||||
|             sys.exit(1) | ||||
| 
 | ||||
|     if _DEBUG: | ||||
|         logger.setLevel(ofunctions.logger_utils.logging.DEBUG) | ||||
| 
 | ||||
|  | @ -305,9 +318,7 @@ This is free software, and you are welcome to redistribute it under certain cond | |||
|             json_error_logging(False, msg, "critical") | ||||
|             sys.exit(70) | ||||
| 
 | ||||
|     aes_key = key_management.get_aes_key() | ||||
| 
 | ||||
|     full_config = npbackup.configuration.load_config(CONFIG_FILE, aes_key) | ||||
|     full_config = npbackup.configuration.load_config(CONFIG_FILE) | ||||
|     if not full_config: | ||||
|         msg = "Cannot obtain repo config" | ||||
|         json_error_logging(False, msg, "critical") | ||||
|  |  | |||
|  | @ -28,6 +28,7 @@ from cryptidy import symmetric_encryption as enc | |||
| from ofunctions.random import random_string | ||||
| from ofunctions.misc import replace_in_iterable, BytesConverter, iter_over_keys | ||||
| from npbackup.customization import ID_STRING | ||||
| from npbackup import key_management | ||||
| 
 | ||||
| 
 | ||||
| sys.path.insert(0, os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))) | ||||
|  | @ -63,6 +64,9 @@ except ImportError: | |||
| 
 | ||||
| 
 | ||||
| logger = getLogger() | ||||
| opt_aes_key = key_management.get_aes_key() | ||||
| if opt_aes_key: | ||||
|     AES_KEY = opt_aes_key | ||||
| 
 | ||||
| 
 | ||||
| # Monkeypatching ruamel.yaml ordreddict so we get to use pseudo dot notations | ||||
|  | @ -301,7 +305,7 @@ def crypt_config( | |||
|         ) | ||||
|     except Exception as exc: | ||||
|         logger.error(f"Cannot {operation} configuration: {exc}.") | ||||
|         logger.info("Trace:", exc_info=True) | ||||
|         logger.debug("Trace:", exc_info=True) | ||||
|         return False | ||||
| 
 | ||||
| 
 | ||||
|  | @ -719,9 +723,8 @@ def _load_config_file(config_file: Path) -> Union[bool, dict]: | |||
|         return False | ||||
| 
 | ||||
| 
 | ||||
| def load_config(config_file: Path, aes_key: bytes = None) -> Optional[dict]: | ||||
|     if not aes_key: | ||||
|         aes_key = AES_KEY | ||||
| def load_config(config_file: Path) -> Optional[dict]: | ||||
| 
 | ||||
|     logger.info(f"Loading configuration file {config_file}") | ||||
| 
 | ||||
|     full_config = _load_config_file(config_file) | ||||
|  | @ -757,7 +760,7 @@ def load_config(config_file: Path, aes_key: bytes = None) -> Optional[dict]: | |||
|         config_file_is_updated = True | ||||
|     # Decrypt variables | ||||
|     full_config = crypt_config( | ||||
|         full_config, aes_key, ENCRYPTED_OPTIONS, operation="decrypt" | ||||
|         full_config, AES_KEY, ENCRYPTED_OPTIONS, operation="decrypt" | ||||
|     ) | ||||
|     if full_config == False: | ||||
|         if EARLIER_AES_KEY: | ||||
|  | @ -791,22 +794,20 @@ def load_config(config_file: Path, aes_key: bytes = None) -> Optional[dict]: | |||
|     return full_config | ||||
| 
 | ||||
| 
 | ||||
| def save_config(config_file: Path, full_config: dict, aes_key: bytes = None) -> bool: | ||||
|     if not aes_key: | ||||
|         aes_key = AES_KEY | ||||
| def save_config(config_file: Path, full_config: dict) -> bool: | ||||
|     try: | ||||
|         with open(config_file, "w", encoding="utf-8") as file_handle: | ||||
|             full_config = inject_permissions_into_full_config(full_config) | ||||
| 
 | ||||
|             if not is_encrypted(full_config): | ||||
|                 full_config = crypt_config( | ||||
|                     full_config, aes_key, ENCRYPTED_OPTIONS, operation="encrypt" | ||||
|                     full_config, AES_KEY, ENCRYPTED_OPTIONS, operation="encrypt" | ||||
|                 ) | ||||
|             yaml = YAML(typ="rt") | ||||
|             yaml.dump(full_config, file_handle) | ||||
|         # Since yaml is a "pointer object", we need to decrypt after saving | ||||
|         full_config = crypt_config( | ||||
|             full_config, aes_key, ENCRYPTED_OPTIONS, operation="decrypt" | ||||
|             full_config, AES_KEY, ENCRYPTED_OPTIONS, operation="decrypt" | ||||
|         ) | ||||
|         # We also need to extract permissions again | ||||
|         full_config = extract_permissions_from_full_config(full_config) | ||||
|  |  | |||
|  | @ -55,12 +55,10 @@ from npbackup.path_helper import CURRENT_DIR | |||
| from npbackup.__version__ import version_string | ||||
| from npbackup.__debug__ import _DEBUG | ||||
| from npbackup.restic_wrapper import ResticRunner | ||||
| from npbackup import key_management | ||||
| 
 | ||||
| 
 | ||||
| logger = getLogger() | ||||
| backend_binary = None | ||||
| aes_key = key_management.get_aes_key() | ||||
| 
 | ||||
| 
 | ||||
| sg.theme(PYSIMPLEGUI_THEME) | ||||
|  | @ -454,7 +452,7 @@ def _main_gui(viewer_mode: bool): | |||
|                 if not values["-config_file-"] or not config_file.exists(): | ||||
|                     sg.PopupError(_t("generic.file_does_not_exist")) | ||||
|                     continue | ||||
|                 full_config = npbackup.configuration.load_config(config_file, aes_key) | ||||
|                 full_config = npbackup.configuration.load_config(config_file) | ||||
|                 if not full_config: | ||||
|                     sg.PopupError(_t("generic.bad_file")) | ||||
|                     continue | ||||
|  | @ -552,7 +550,7 @@ def _main_gui(viewer_mode: bool): | |||
|         Load config file until we got something | ||||
|         """ | ||||
|         if config_file: | ||||
|             full_config = npbackup.configuration.load_config(config_file, aes_key) | ||||
|             full_config = npbackup.configuration.load_config(config_file) | ||||
|             if not config_file.exists(): | ||||
|                 config_file = None | ||||
|             if not full_config: | ||||
|  | @ -572,7 +570,7 @@ def _main_gui(viewer_mode: bool): | |||
|                     ) | ||||
|             if config_file: | ||||
|                 logger.info(f"Using configuration file {config_file}") | ||||
|                 full_config = npbackup.configuration.load_config(config_file, aes_key) | ||||
|                 full_config = npbackup.configuration.load_config(config_file) | ||||
|                 if not full_config: | ||||
|                     sg.PopupError(f"{_t('main_gui.config_error')} {config_file}") | ||||
|                     config_file = None | ||||
|  |  | |||
|  | @ -31,13 +31,11 @@ from npbackup.customization import ( | |||
|     TREE_ICON, | ||||
|     INHERITED_TREE_ICON, | ||||
| ) | ||||
| from npbackup import key_management | ||||
| 
 | ||||
| if os.name == "nt": | ||||
|     from npbackup.windows.task import create_scheduled_task | ||||
| 
 | ||||
| logger = getLogger() | ||||
| aes_key = key_management.get_aes_key() | ||||
| 
 | ||||
| 
 | ||||
| # Monkeypatching PySimpleGUI | ||||
|  | @ -1924,7 +1922,7 @@ def config_gui(full_config: dict, config_file: str): | |||
|             full_config = update_config_dict( | ||||
|                 full_config, current_object_type, current_object_name, values | ||||
|             ) | ||||
|             result = configuration.save_config(config_file, full_config, aes_key) | ||||
|             result = configuration.save_config(config_file, full_config) | ||||
|             if result: | ||||
|                 sg.Popup(_t("config_gui.configuration_saved"), keep_on_top=True) | ||||
|                 logger.info("Configuration saved successfully.") | ||||
|  |  | |||
|  | @ -7,10 +7,15 @@ __intname__ = "npbackup.get_key" | |||
| 
 | ||||
| 
 | ||||
| import os | ||||
| from logging import getLogger | ||||
| from command_runner import command_runner | ||||
| from cryptidy.symmetric_encryption import generate_key | ||||
| from npbackup.obfuscation import obfuscation | ||||
| 
 | ||||
| 
 | ||||
| logger = getLogger() | ||||
| 
 | ||||
| 
 | ||||
| def get_aes_key(): | ||||
|     """ | ||||
|     Get encryption key from environment variable or file | ||||
|  | @ -28,9 +33,20 @@ def get_aes_key(): | |||
|     else: | ||||
|         key_command = os.environ.get("NPBACKUP_KEY_COMMAND", None) | ||||
|         if key_command: | ||||
|             exit_code, output = command_runner(key_command, shell=True) | ||||
|             exit_code, output = command_runner(key_command, encoding=False, shell=True) | ||||
|             if exit_code != 0: | ||||
|                 msg = f"Cannot run encryption key command: {output}" | ||||
|                 return False, msg | ||||
|             key = output | ||||
|             key = bytes(output) | ||||
|     return obfuscation(key) | ||||
| 
 | ||||
| 
 | ||||
| def create_key_file(key_location: str): | ||||
|     try: | ||||
|         with open(key_location, "wb") as key_file: | ||||
|             key_file.write(obfuscation(generate_key())) | ||||
|             logger.info(f"Encryption key file created at {key_location}") | ||||
|             return True | ||||
|     except OSError as exc: | ||||
|         logger.critical("Cannot create encryption key file: {exc}") | ||||
|         return False | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ __intname__ = "npbackup.secret_keys" | |||
| __author__ = "Orsiris de Jong" | ||||
| __copyright__ = "Copyright (C) 2023-2024 NetInvent" | ||||
| __license__ = "GPL-3.0-only" | ||||
| __build__ = "2023120601" | ||||
| __build__ = "2024050901" | ||||
| 
 | ||||
| 
 | ||||
| # Encryption key to keep repo settings safe in plain text yaml config file | ||||
|  | @ -15,8 +15,7 @@ __build__ = "2023120601" | |||
| # This is the default key that comes with NPBackup.. You should change it (and keep a backup copy in case you need to decrypt a config file data) | ||||
| # You can overwrite this by copying this file to `../PRIVATE/_private_secret_keys.py` and generating a new key | ||||
| # Obtain a new key with: | ||||
| # from cryptidy.symmetric_encryption import generate_key | ||||
| # print(generate_key(32)) | ||||
| # npbackup-cli --create-key keyfile.key | ||||
| 
 | ||||
| AES_KEY = b"\xc3T\xdci\xe3[s\x87o\x96\x8f\xe5\xee.>\xf1,\x94\x8d\xfe\x0f\xea\x11\x05 \xa0\xe9S\xcf\x82\xad|" | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue