Implement --create-key and fix alternative key usage, closes #56

This commit is contained in:
deajan 2024-05-09 17:23:54 +02:00
parent dd13d9520e
commit 748bafb2b4
8 changed files with 69 additions and 30 deletions

View file

@ -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.

View file

@ -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

View file

@ -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")

View file

@ -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)

View file

@ -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

View file

@ -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.")

View file

@ -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

View file

@ -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|"