mirror of
https://github.com/netinvent/npbackup.git
synced 2025-10-27 22:08:07 +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