Add optional file encryption for user credentials configuration (#3391)
* Optional encryption of user credentials configuration file * Update README * Update * Update
This commit is contained in:
parent
b8923807c7
commit
fae210dfea
|
|
@ -46,6 +46,7 @@ repos:
|
||||||
- pydantic
|
- pydantic
|
||||||
- pydantic-settings
|
- pydantic-settings
|
||||||
- pytest
|
- pytest
|
||||||
|
- cryptography
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.11.7
|
rev: v0.11.7
|
||||||
hooks:
|
hooks:
|
||||||
|
|
|
||||||
1
PKGBUILD
1
PKGBUILD
|
|
@ -34,6 +34,7 @@ depends=(
|
||||||
'ntfs-3g'
|
'ntfs-3g'
|
||||||
)
|
)
|
||||||
makedepends=(
|
makedepends=(
|
||||||
|
'python-cryptography'
|
||||||
'python-setuptools'
|
'python-setuptools'
|
||||||
'python-sphinx'
|
'python-sphinx'
|
||||||
'python-build'
|
'python-build'
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -56,6 +56,18 @@ To load the configuration file into `archinstall` run the following command
|
||||||
archinstall --config <path to user config file or URL> --creds <path to user credentials config file or URL>
|
archinstall --config <path to user config file or URL> --creds <path to user credentials config file or URL>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Credentials configuration file encryption
|
||||||
|
By default all user account credentials are hashed with `yescrypt` and only the hash is stored in the saved `user_credentials.json` file.
|
||||||
|
This is not possible for disk encryption password which needs to be stored in plaintext to be able to apply it.
|
||||||
|
|
||||||
|
However, when selecting to save configuration files, `archinstall` will prompt for the option to encrypt the `user_credentials.json` file content.
|
||||||
|
A prompt will require to enter a encryption password to encrypt the file. When providing an encrypted `user_configuration.json` as a argument with `--creds <user_credentials.json>`
|
||||||
|
there are multiple ways to provide the decryption key:
|
||||||
|
* Provide the decryption key via the command line argument `--creds-decryption-key <password>`
|
||||||
|
* Store the encryption key in the environment variable `ARCHINSTALL_CREDS_DECRYPTION_KEY` which will be read automatically
|
||||||
|
* If none of the above is provided a prompt will be shown to enter the decryption key manually
|
||||||
|
|
||||||
|
|
||||||
# Help or Issues
|
# Help or Issues
|
||||||
|
|
||||||
If you come across any issues, kindly submit your issue here on Github or post your query in the
|
If you come across any issues, kindly submit your issue here on Github or post your query in the
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,6 @@ if TYPE_CHECKING:
|
||||||
_: Callable[[str], DeferredTranslation]
|
_: Callable[[str], DeferredTranslation]
|
||||||
|
|
||||||
|
|
||||||
# add the custom _ as a builtin, it can now be used anywhere in the
|
|
||||||
# project to mark strings as translatable with _('translate me')
|
|
||||||
DeferredTranslation.install()
|
|
||||||
|
|
||||||
|
|
||||||
# @archinstall.plugin decorator hook to programmatically add
|
# @archinstall.plugin decorator hook to programmatically add
|
||||||
# plugins in runtime. Useful in profiles_bck and other things.
|
# plugins in runtime. Useful in profiles_bck and other things.
|
||||||
def plugin(f, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
def plugin(f, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from argparse import ArgumentParser, Namespace
|
from argparse import ArgumentParser, Namespace
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from importlib.metadata import version
|
from importlib.metadata import version
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
from pydantic.dataclasses import dataclass as p_dataclass
|
from pydantic.dataclasses import dataclass as p_dataclass
|
||||||
|
|
||||||
|
from archinstall.lib.crypt import decrypt
|
||||||
from archinstall.lib.models.audio_configuration import AudioConfiguration
|
from archinstall.lib.models.audio_configuration import AudioConfiguration
|
||||||
from archinstall.lib.models.bootloader import Bootloader
|
from archinstall.lib.models.bootloader import Bootloader
|
||||||
from archinstall.lib.models.device_model import DiskEncryption, DiskLayoutConfiguration
|
from archinstall.lib.models.device_model import DiskEncryption, DiskLayoutConfiguration
|
||||||
|
|
@ -20,10 +22,19 @@ from archinstall.lib.models.network_configuration import NetworkConfiguration
|
||||||
from archinstall.lib.models.packages import Repository
|
from archinstall.lib.models.packages import Repository
|
||||||
from archinstall.lib.models.profile_model import ProfileConfiguration
|
from archinstall.lib.models.profile_model import ProfileConfiguration
|
||||||
from archinstall.lib.models.users import Password, User
|
from archinstall.lib.models.users import Password, User
|
||||||
from archinstall.lib.output import error, warn
|
from archinstall.lib.output import debug, error, warn
|
||||||
from archinstall.lib.plugins import load_plugin
|
from archinstall.lib.plugins import load_plugin
|
||||||
from archinstall.lib.storage import storage
|
from archinstall.lib.storage import storage
|
||||||
from archinstall.lib.translationhandler import Language, translation_handler
|
from archinstall.lib.translationhandler import Language, translation_handler
|
||||||
|
from archinstall.lib.utils.util import get_password
|
||||||
|
from archinstall.tui.curses_menu import Tui
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from archinstall.lib.translationhandler import DeferredTranslation
|
||||||
|
|
||||||
|
_: Callable[[str], DeferredTranslation]
|
||||||
|
|
||||||
|
|
||||||
@p_dataclass
|
@p_dataclass
|
||||||
|
|
@ -32,6 +43,7 @@ class Arguments:
|
||||||
config_url: str | None = None
|
config_url: str | None = None
|
||||||
creds: Path | None = None
|
creds: Path | None = None
|
||||||
creds_url: str | None = None
|
creds_url: str | None = None
|
||||||
|
creds_decryption_key: str | None = None
|
||||||
silent: bool = False
|
silent: bool = False
|
||||||
dry_run: bool = False
|
dry_run: bool = False
|
||||||
script: str = 'guided'
|
script: str = 'guided'
|
||||||
|
|
@ -274,6 +286,13 @@ class ArchConfigHandler:
|
||||||
default=None,
|
default=None,
|
||||||
help="Url to a JSON credentials configuration file"
|
help="Url to a JSON credentials configuration file"
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--creds-decryption-key",
|
||||||
|
type=str,
|
||||||
|
nargs="?",
|
||||||
|
default=None,
|
||||||
|
help="Decryption key for credentials file"
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--silent",
|
"--silent",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|
@ -370,6 +389,10 @@ class ArchConfigHandler:
|
||||||
plugin_path = Path(args.plugin)
|
plugin_path = Path(args.plugin)
|
||||||
load_plugin(plugin_path)
|
load_plugin(plugin_path)
|
||||||
|
|
||||||
|
if args.creds_decryption_key is None:
|
||||||
|
if os.environ.get('ARCHINSTALL_CREDS_DECRYPTION_KEY'):
|
||||||
|
args.creds_decryption_key = os.environ.get('ARCHINSTALL_CREDS_DECRYPTION_KEY')
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def _parse_config(self) -> dict[str, Any]:
|
def _parse_config(self) -> dict[str, Any]:
|
||||||
|
|
@ -391,12 +414,57 @@ class ArchConfigHandler:
|
||||||
creds_data = self._fetch_from_url(self._args.creds_url)
|
creds_data = self._fetch_from_url(self._args.creds_url)
|
||||||
|
|
||||||
if creds_data is not None:
|
if creds_data is not None:
|
||||||
config.update(json.loads(creds_data))
|
json_data = self._process_creds_data(creds_data)
|
||||||
|
if json_data is not None:
|
||||||
|
config.update(json_data)
|
||||||
|
|
||||||
config = self._cleanup_config(config)
|
config = self._cleanup_config(config)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
def _process_creds_data(self, creds_data: str) -> dict[str, Any] | None:
|
||||||
|
if creds_data.startswith('$'): # encrypted data
|
||||||
|
if self._args.creds_decryption_key is not None:
|
||||||
|
try:
|
||||||
|
creds_data = decrypt(creds_data, self._args.creds_decryption_key)
|
||||||
|
return json.loads(creds_data)
|
||||||
|
except ValueError as err:
|
||||||
|
if 'Invalid password' in str(err):
|
||||||
|
error(str(_('Incorrect credentials file decryption password')))
|
||||||
|
exit(1)
|
||||||
|
else:
|
||||||
|
debug(f'Error decrypting credentials file: {err}')
|
||||||
|
raise err from err
|
||||||
|
else:
|
||||||
|
incorrect_password = False
|
||||||
|
|
||||||
|
with Tui():
|
||||||
|
while True:
|
||||||
|
header = str(_('Incorrect password')) if incorrect_password else None
|
||||||
|
|
||||||
|
decryption_pwd = get_password(
|
||||||
|
text=str(_('Credentials file decryption password')),
|
||||||
|
header=header,
|
||||||
|
allow_skip=False,
|
||||||
|
skip_confirmation=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not decryption_pwd:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
creds_data = decrypt(creds_data, decryption_pwd.plaintext)
|
||||||
|
break
|
||||||
|
except ValueError as err:
|
||||||
|
if 'Invalid password' in str(err):
|
||||||
|
debug('Incorrect credentials file decryption password')
|
||||||
|
incorrect_password = True
|
||||||
|
else:
|
||||||
|
debug(f'Error decrypting credentials file: {err}')
|
||||||
|
raise err from err
|
||||||
|
|
||||||
|
return json.loads(creds_data)
|
||||||
|
|
||||||
def _fetch_from_url(self, url: str) -> str:
|
def _fetch_from_url(self, url: str) -> str:
|
||||||
if urllib.parse.urlparse(url).scheme:
|
if urllib.parse.urlparse(url).scheme:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,11 @@ from archinstall.tui.result import ResultType
|
||||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation, PreviewStyle
|
from archinstall.tui.types import Alignment, FrameProperties, Orientation, PreviewStyle
|
||||||
|
|
||||||
from .args import ArchConfig
|
from .args import ArchConfig
|
||||||
|
from .crypt import encrypt
|
||||||
from .general import JSON, UNSAFE_JSON
|
from .general import JSON, UNSAFE_JSON
|
||||||
from .output import debug, warn
|
from .output import debug, warn
|
||||||
from .storage import storage
|
from .storage import storage
|
||||||
from .utils.util import prompt_dir
|
from .utils.util import get_password, prompt_dir
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
@ -100,19 +101,33 @@ class ConfigurationOutput:
|
||||||
target.write_text(self.user_config_to_json())
|
target.write_text(self.user_config_to_json())
|
||||||
target.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
|
target.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
|
||||||
|
|
||||||
def save_user_creds(self, dest_path: Path) -> None:
|
def save_user_creds(
|
||||||
|
self,
|
||||||
|
dest_path: Path,
|
||||||
|
password: str | None = None
|
||||||
|
) -> None:
|
||||||
|
data = self.user_credentials_to_json()
|
||||||
|
|
||||||
|
if password:
|
||||||
|
data = encrypt(password, data)
|
||||||
|
|
||||||
if self._is_valid_path(dest_path):
|
if self._is_valid_path(dest_path):
|
||||||
target = dest_path / self._user_creds_file
|
target = dest_path / self._user_creds_file
|
||||||
target.write_text(self.user_credentials_to_json())
|
target.write_text(data)
|
||||||
target.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
|
target.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
|
||||||
|
|
||||||
def save(self, dest_path: Path | None = None, creds: bool = False) -> None:
|
def save(
|
||||||
|
self,
|
||||||
|
dest_path: Path | None = None,
|
||||||
|
creds: bool = False,
|
||||||
|
password: str | None = None
|
||||||
|
) -> None:
|
||||||
save_path = dest_path or self._default_save_path
|
save_path = dest_path or self._default_save_path
|
||||||
|
|
||||||
if self._is_valid_path(save_path):
|
if self._is_valid_path(save_path):
|
||||||
self.save_user_config(save_path)
|
self.save_user_config(save_path)
|
||||||
if creds:
|
if creds:
|
||||||
self.save_user_creds(save_path)
|
self.save_user_creds(save_path, password=password)
|
||||||
|
|
||||||
|
|
||||||
def save_config(config: ArchConfig) -> None:
|
def save_config(config: ArchConfig) -> None:
|
||||||
|
|
@ -202,10 +217,36 @@ def save_config(config: ArchConfig) -> None:
|
||||||
|
|
||||||
debug(f"Saving configuration files to {dest_path.absolute()}")
|
debug(f"Saving configuration files to {dest_path.absolute()}")
|
||||||
|
|
||||||
|
header = str(_('Do you want to encrypt the user_credentials.json file?'))
|
||||||
|
|
||||||
|
group = MenuItemGroup.yes_no()
|
||||||
|
group.focus_item = MenuItem.no()
|
||||||
|
|
||||||
|
result = SelectMenu(
|
||||||
|
group,
|
||||||
|
header=header,
|
||||||
|
allow_skip=False,
|
||||||
|
alignment=Alignment.CENTER,
|
||||||
|
columns=2,
|
||||||
|
orientation=Orientation.HORIZONTAL
|
||||||
|
).run()
|
||||||
|
|
||||||
|
enc_password: str | None = None
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Selection:
|
||||||
|
if result.item() == MenuItem.yes():
|
||||||
|
password = get_password(
|
||||||
|
text=str(_('Credentials file encryption password')),
|
||||||
|
allow_skip=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if password:
|
||||||
|
enc_password = password.plaintext
|
||||||
|
|
||||||
match save_option:
|
match save_option:
|
||||||
case "user_config":
|
case "user_config":
|
||||||
config_output.save_user_config(dest_path)
|
config_output.save_user_config(dest_path)
|
||||||
case "user_creds":
|
case "user_creds":
|
||||||
config_output.save_user_creds(dest_path)
|
config_output.save_user_creds(dest_path, password=enc_password)
|
||||||
case "all":
|
case "all":
|
||||||
config_output.save(dest_path, creds=True)
|
config_output.save(dest_path, creds=True, password=enc_password)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
|
import base64
|
||||||
import ctypes
|
import ctypes
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
|
from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
|
||||||
|
|
||||||
from .output import debug
|
from .output import debug
|
||||||
|
|
||||||
libcrypt = ctypes.CDLL("libcrypt.so")
|
libcrypt = ctypes.CDLL("libcrypt.so")
|
||||||
|
|
@ -69,3 +74,52 @@ def crypt_yescrypt(plaintext: str) -> str:
|
||||||
raise ValueError('crypt() returned NULL')
|
raise ValueError('crypt() returned NULL')
|
||||||
|
|
||||||
return crypt_hash.decode('utf-8')
|
return crypt_hash.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_fernet(salt: bytes, password: str) -> Fernet:
|
||||||
|
# https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#argon2id
|
||||||
|
kdf = Argon2id(
|
||||||
|
salt=salt,
|
||||||
|
length=32,
|
||||||
|
iterations=1,
|
||||||
|
lanes=4,
|
||||||
|
memory_cost=64 * 1024,
|
||||||
|
ad=None,
|
||||||
|
secret=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
key = base64.urlsafe_b64encode(
|
||||||
|
kdf.derive(
|
||||||
|
password.encode('utf-8')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Fernet(key)
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(password: str, data: str) -> str:
|
||||||
|
salt = os.urandom(16)
|
||||||
|
f = _get_fernet(salt, password)
|
||||||
|
token = f.encrypt(data.encode('utf-8'))
|
||||||
|
|
||||||
|
encoded_token = base64.urlsafe_b64encode(token).decode('utf-8')
|
||||||
|
encoded_salt = base64.urlsafe_b64encode(salt).decode('utf-8')
|
||||||
|
|
||||||
|
return f'$argon2id${encoded_salt}${encoded_token}'
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt(data: str, password: str):
|
||||||
|
_, algo, encoded_salt, encoded_token = data.split('$')
|
||||||
|
salt = base64.urlsafe_b64decode(encoded_salt)
|
||||||
|
token = base64.urlsafe_b64decode(encoded_token)
|
||||||
|
|
||||||
|
if algo != 'argon2id':
|
||||||
|
raise ValueError(f'Unsupported algorithm {algo!r}')
|
||||||
|
|
||||||
|
f = _get_fernet(salt, password)
|
||||||
|
try:
|
||||||
|
decrypted = f.decrypt(token)
|
||||||
|
except InvalidToken:
|
||||||
|
raise ValueError('Invalid password')
|
||||||
|
|
||||||
|
return decrypted.decode('utf-8')
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import builtins
|
||||||
import gettext
|
import gettext
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
@ -190,10 +191,7 @@ class DeferredTranslation:
|
||||||
def format(self, *args) -> str:
|
def format(self, *args) -> str:
|
||||||
return self.message.format(*args)
|
return self.message.format(*args)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def install(cls) -> None:
|
|
||||||
import builtins
|
|
||||||
builtins._ = cls # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
|
builtins._ = DeferredTranslation # type: ignore[attr-defined]
|
||||||
|
|
||||||
translation_handler = TranslationHandler()
|
translation_handler = TranslationHandler()
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ def get_password(
|
||||||
text: str,
|
text: str,
|
||||||
header: str | None = None,
|
header: str | None = None,
|
||||||
allow_skip: bool = False,
|
allow_skip: bool = False,
|
||||||
preset: str | None = None
|
preset: str | None = None,
|
||||||
|
skip_confirmation: bool = False
|
||||||
) -> Password | None:
|
) -> Password | None:
|
||||||
failure: str | None = None
|
failure: str | None = None
|
||||||
|
|
||||||
|
|
@ -44,6 +45,9 @@ def get_password(
|
||||||
|
|
||||||
password = Password(plaintext=result.text())
|
password = Password(plaintext=result.text())
|
||||||
|
|
||||||
|
if skip_confirmation:
|
||||||
|
return password
|
||||||
|
|
||||||
if header is not None:
|
if header is not None:
|
||||||
confirmation_header = f'{header}{_("Password")}: {password.hidden()}\n'
|
confirmation_header = f'{header}{_("Password")}: {password.hidden()}\n'
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ classifiers = [
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyparted @ https://github.com//dcantrell/pyparted/archive/v3.13.0.tar.gz#sha512=26819e28d73420937874f52fda03eb50ab1b136574ea9867a69d46ae4976d38c4f26a2697fa70597eed90dd78a5ea209bafcc3227a17a7a5d63cff6d107c2b11",
|
"pyparted @ https://github.com//dcantrell/pyparted/archive/v3.13.0.tar.gz#sha512=26819e28d73420937874f52fda03eb50ab1b136574ea9867a69d46ae4976d38c4f26a2697fa70597eed90dd78a5ea209bafcc3227a17a7a5d63cff6d107c2b11",
|
||||||
"pydantic==2.11.3"
|
"pydantic==2.11.3",
|
||||||
|
"cryptography>=44.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ def creds_fixture() -> Path:
|
||||||
return Path(__file__).parent / 'data' / 'test_creds.json'
|
return Path(__file__).parent / 'data' / 'test_creds.json'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def encrypted_creds_fixture() -> Path:
|
||||||
|
return Path(__file__).parent / 'data' / 'test_encrypted_creds.json'
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
def deprecated_creds_config() -> Path:
|
def deprecated_creds_config() -> Path:
|
||||||
return Path(__file__).parent / 'data' / 'test_deprecated_creds_config.json'
|
return Path(__file__).parent / 'data' / 'test_deprecated_creds_config.json'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
$argon2id$9lv5DTin0wusAc0tFPxbkw==$Z0FBQUFBQm4temxTbzJnd09OQmVCbU1DTGg0akNBMXNoeFI1UTlHVGRiVzF0UUFDRW9rVDRpeDVaVENVb1NGMzhZc0RReEZ4MmNtdkc3dHctU3BlNXdXb01UVWJibnYwZmVYRXZkbk9TQlUxUTVkN3Z6NWRfUTNVYlVUS3lzckhpNERJeW5mOUcxdnJqU2loVl95dlRBdWdEZXlCOVZyRHZRaEk2NURUWGRROGpEeExpdWtGU3ZTQ0FqTFFEMEozMmJEQkxabW1Wcjg2cXdEVllfYXYzN0p0eE9PRHVkZnFNcWZnY2h3cVJhVjA3S1Q2MER5RWZrb0FUTnJobW5icERzYUNVZE5iT3VLLWFtLTFDZVdoMUFub3FGQzBHcGFFNVRVbTBZM2ZqXzRERGlvSEJndWFma25hYlpvOHllUEVOZUVmc3dCN215NjlrdHVYNElfWGl5Ny1xTVlRWWw3V0VTSGRONENWbTdPalMxY1BnUGs3eFZoRERnOXNGSW9zRGZub2xQSEZSODFKVGxtdzNyZlpfeHdMSkJZUUNPQUZUWHNEd25KVWpwZTV2LVNyb1pzSFF6SGg3c3RldEpwLUVnQ0w1US1mN2l6MmdVLTZSSHpvNVdtcWhTaHVXMGJUczBFN2s0VXctUEo1OUdzNUs4bW9sbnpfcmJmQzNxSVNPM2NVeWktek5QaFU=
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from pytest import MonkeyPatch
|
from pytest import MonkeyPatch
|
||||||
|
|
@ -26,6 +27,8 @@ def test_default_args(monkeypatch: MonkeyPatch) -> None:
|
||||||
config=None,
|
config=None,
|
||||||
config_url=None,
|
config_url=None,
|
||||||
creds=None,
|
creds=None,
|
||||||
|
creds_url=None,
|
||||||
|
creds_decryption_key=None,
|
||||||
silent=False,
|
silent=False,
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
script='guided',
|
script='guided',
|
||||||
|
|
@ -260,3 +263,51 @@ def test_deprecated_creds_config_parsing(
|
||||||
groups=['wheel']
|
groups=['wheel']
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_encrypted_creds_with_arg(
|
||||||
|
monkeypatch: MonkeyPatch,
|
||||||
|
encrypted_creds_fixture: Path,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr('sys.argv', [
|
||||||
|
'archinstall',
|
||||||
|
'--creds', str(encrypted_creds_fixture),
|
||||||
|
'--creds-decryption-key', 'master'
|
||||||
|
])
|
||||||
|
|
||||||
|
handler = ArchConfigHandler()
|
||||||
|
arch_config = handler.config
|
||||||
|
|
||||||
|
assert arch_config.root_enc_password == Password(enc_password='$y$j9T$FWCInXmSsS.8KV4i7O50H.$Hb6/g.Sw1ry888iXgkVgc93YNuVk/Rw94knDKdPVQw7')
|
||||||
|
assert arch_config.users == [
|
||||||
|
User(
|
||||||
|
username='t',
|
||||||
|
password=Password(enc_password='$y$j9T$3KxMigAEnjtzbjalhLewE.$gmuoQtc9RNY/PmO/GxHHYvkZNO86Eeftg1Oc7L.QSO/'),
|
||||||
|
sudo=True,
|
||||||
|
groups=[]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_encrypted_creds_with_env_var(
|
||||||
|
monkeypatch: MonkeyPatch,
|
||||||
|
encrypted_creds_fixture: Path,
|
||||||
|
) -> None:
|
||||||
|
os.environ['ARCHINSTALL_CREDS_DECRYPTION_KEY'] = 'master'
|
||||||
|
monkeypatch.setattr('sys.argv', [
|
||||||
|
'archinstall',
|
||||||
|
'--creds', str(encrypted_creds_fixture),
|
||||||
|
])
|
||||||
|
|
||||||
|
handler = ArchConfigHandler()
|
||||||
|
arch_config = handler.config
|
||||||
|
|
||||||
|
assert arch_config.root_enc_password == Password(enc_password='$y$j9T$FWCInXmSsS.8KV4i7O50H.$Hb6/g.Sw1ry888iXgkVgc93YNuVk/Rw94knDKdPVQw7')
|
||||||
|
assert arch_config.users == [
|
||||||
|
User(
|
||||||
|
username='t',
|
||||||
|
password=Password(enc_password='$y$j9T$3KxMigAEnjtzbjalhLewE.$gmuoQtc9RNY/PmO/GxHHYvkZNO86Eeftg1Oc7L.QSO/'),
|
||||||
|
sudo=True,
|
||||||
|
groups=[]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue