Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions archinstall/lib/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from archinstall.lib.models.network_configuration import NetworkConfiguration
from archinstall.lib.models.packages import Repository
from archinstall.lib.models.profile_model import ProfileConfiguration
from archinstall.lib.models.users import Password, User
from archinstall.lib.models.users import Password, User, UserSerialization
from archinstall.lib.output import debug, error, logger, warn
from archinstall.lib.plugins import load_plugin
from archinstall.lib.translationhandler import Language, tr, translation_handler
Expand Down Expand Up @@ -78,14 +78,15 @@ class ArchConfig:

# Special fields that should be handle with care due to security implications
users: list[User] = field(default_factory=list)
root_enc_password: Password | None = None

def unsafe_json(self) -> dict[str, Any]:
config = {
config: dict[str, list[UserSerialization] | str | None] = {
'users': [user.json() for user in self.users],
'root_enc_password': self.root_enc_password.enc_password if self.root_enc_password else None,
}

if self.auth_config and self.auth_config.root_enc_password:
config['root_enc_password'] = self.auth_config.root_enc_password.enc_password

if self.disk_config:
disk_encryption = self.disk_config.disk_encryption
if disk_encryption and disk_encryption.encryption_password:
Expand Down Expand Up @@ -222,11 +223,17 @@ def from_config(cls, args_config: dict[str, Any]) -> 'ArchConfig':
arch_config.services = services

# DEPRECATED: backwards compatibility
root_password = None
if root_password := args_config.get('!root-password', None):
arch_config.root_enc_password = Password(plaintext=root_password)
root_password = Password(plaintext=root_password)

if enc_password := args_config.get('root_enc_password', None):
arch_config.root_enc_password = Password(enc_password=enc_password)
root_password = Password(enc_password=enc_password)

if root_password is not None:
if arch_config.auth_config is None:
arch_config.auth_config = AuthenticationConfiguration()
arch_config.auth_config.root_enc_password = root_password

if custom_commands := args_config.get('custom_commands', []):
arch_config.custom_commands = custom_commands
Expand Down
19 changes: 19 additions & 0 deletions archinstall/lib/authentication/authentication_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from archinstall.lib.disk.fido import Fido2
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import Password
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.util import get_password
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
Expand Down Expand Up @@ -33,6 +35,12 @@ def run(self, additional_title: str | None = None) -> AuthenticationConfiguratio

def _define_menu_options(self) -> list[MenuItem]:
return [
MenuItem(
text=tr('Root password'),
action=select_root_password,
preview_action=self._prev_root_pwd,
key='root_enc_password',
),
MenuItem(
text=tr('U2F login setup'),
action=setup_u2f_login,
Expand All @@ -43,6 +51,12 @@ def _define_menu_options(self) -> list[MenuItem]:
),
]

def _prev_root_pwd(self, item: MenuItem) -> str | None:
if item.value is not None:
password: Password = item.value
return f'{tr("Root password")}: {password.hidden()}'
return None

def _depends_on_u2f(self) -> bool:
devices = Fido2.get_fido2_devices()
if not devices:
Expand All @@ -63,6 +77,11 @@ def _prev_u2f_login(self, item: MenuItem) -> str | None:
return None


def select_root_password(preset: str | None = None) -> Password | None:
password = get_password(text=tr('Root password'), allow_skip=True)
return password


def setup_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | None:
items = []
for method in U2FLoginMethod:
Expand Down
38 changes: 13 additions & 25 deletions archinstall/lib/global_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,10 @@
from .models.network_configuration import NetworkConfiguration, NicType
from .models.packages import Repository
from .models.profile_model import ProfileConfiguration
from .models.users import Password, User
from .models.users import User
from .output import FormattedOutput
from .pacman.config import PacmanConfig
from .translationhandler import Language, tr, translation_handler
from .utils.util import get_password


class GlobalMenu(AbstractMenu[None]):
Expand Down Expand Up @@ -110,16 +109,9 @@ def _get_menu_options(self) -> list[MenuItem]:
preview_action=self._prev_hostname,
key='hostname',
),
MenuItem(
text=tr('Root password'),
action=self._set_root_password,
preview_action=self._prev_root_pwd,
key='root_enc_password',
),
MenuItem(
text=tr('Authentication'),
action=self._select_authentication,
value=[],
preview_action=self._prev_authentication,
key='auth_config',
),
Expand Down Expand Up @@ -230,13 +222,16 @@ def has_superuser() -> bool:

missing = set()

item: MenuItem = self._item_group.find_by_key('auth_config')
auth_config: AuthenticationConfiguration | None = item.value

if (auth_config is None or auth_config.root_enc_password is None) and not has_superuser():
missing.add(
tr('Either root-password or at least 1 user with sudo privileges must be specified'),
)

for item in self._item_group.items:
if item.key in ['root_enc_password', 'users']:
if not check('root_enc_password') and not has_superuser():
missing.add(
tr('Either root-password or at least 1 user with sudo privileges must be specified'),
)
elif item.mandatory:
if item.mandatory:
assert item.key is not None
if not check(item.key):
missing.add(item.text)
Expand Down Expand Up @@ -314,6 +309,9 @@ def _prev_authentication(self, item: MenuItem) -> str | None:
auth_config: AuthenticationConfiguration = item.value
output = ''

if auth_config.root_enc_password:
output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n'

if auth_config.u2f_config:
u2f_config = auth_config.u2f_config
login_method = u2f_config.u2f_login_method.display_value()
Expand Down Expand Up @@ -400,12 +398,6 @@ def _prev_hostname(self, item: MenuItem) -> str | None:
return f'{tr("Hostname")}: {item.value}'
return None

def _prev_root_pwd(self, item: MenuItem) -> str | None:
if item.value is not None:
password: Password = item.value
return f'{tr("Root password")}: {password.hidden()}'
return None

def _prev_parallel_dw(self, item: MenuItem) -> str | None:
if item.value is not None:
return f'{tr("Parallel Downloads")}: {item.value}'
Expand Down Expand Up @@ -510,10 +502,6 @@ def _prev_profile(self, item: MenuItem) -> str | None:

return None

def _set_root_password(self, preset: str | None = None) -> Password | None:
password = get_password(text=tr('Root password'), allow_skip=True)
return password

def _select_disk_config(
self,
preset: DiskLayoutConfiguration | None = None,
Expand Down
5 changes: 5 additions & 0 deletions archinstall/lib/models/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from enum import Enum
from typing import Any, NotRequired, TypedDict

from archinstall.lib.models.users import Password
from archinstall.lib.translationhandler import tr


Expand Down Expand Up @@ -58,6 +59,7 @@ def parse_arg(args: U2FLoginConfigSerialization) -> 'U2FLoginConfiguration | Non

@dataclass
class AuthenticationConfiguration:
root_enc_password: Password | None = None
u2f_config: U2FLoginConfiguration | None = None

@staticmethod
Expand All @@ -67,6 +69,9 @@ def parse_arg(args: dict[str, Any]) -> 'AuthenticationConfiguration':
if (u2f_config := args.get('u2f_config')) is not None:
auth_config.u2f_config = U2FLoginConfiguration.parse_arg(u2f_config)

if enc_password := args.get('root_enc_password'):
auth_config.root_enc_password = Password(enc_password=enc_password)

return auth_config

def json(self) -> AuthenticationSerialization:
Expand Down
8 changes: 4 additions & 4 deletions archinstall/lib/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ def _check_password_strength(
return PasswordStrength.VERY_WEAK


_UserSerialization = TypedDict(
'_UserSerialization',
UserSerialization = TypedDict(
'UserSerialization',
{
'username': str,
'!password': NotRequired[str],
Expand Down Expand Up @@ -173,7 +173,7 @@ def table_data(self) -> dict[str, str | bool | list[str]]:
'groups': self.groups,
}

def json(self) -> _UserSerialization:
def json(self) -> UserSerialization:
return {
'username': self.username,
'enc_password': self.password.enc_password,
Expand All @@ -184,7 +184,7 @@ def json(self) -> _UserSerialization:
@classmethod
def parse_arguments(
cls,
args: list[_UserSerialization],
args: list[UserSerialization],
) -> list['User']:
users: list[User] = []

Expand Down
4 changes: 2 additions & 2 deletions archinstall/scripts/guided.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ def perform_installation(mountpoint: Path) -> None:
if accessibility_tools_in_use():
installation.enable_espeakup()

if root_pw := config.root_enc_password:
root_user = User('root', root_pw, False)
if config.auth_config and config.auth_config.root_enc_password:
root_user = User('root', config.auth_config.root_enc_password, False)
installation.set_user_password(root_user)

if (profile_config := config.profile_config) and profile_config.profile:
Expand Down
11 changes: 7 additions & 4 deletions tests/test_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ def test_config_file_parsing(
audio_config=AudioConfiguration(audio=Audio.PIPEWIRE),
),
auth_config=AuthenticationConfiguration(
root_enc_password=Password(enc_password='password_hash'),
u2f_config=U2FLoginConfiguration(
u2f_login_method=U2FLoginMethod.Passwordless,
passwordless_sudo=True,
Expand Down Expand Up @@ -223,7 +224,6 @@ def test_config_file_parsing(
),
],
services=['service_1', 'service_2'],
root_enc_password=Password(enc_password='password_hash'),
custom_commands=["echo 'Hello, World!'"],
)

Expand Down Expand Up @@ -280,7 +280,8 @@ def test_deprecated_creds_config_parsing(
handler = ArchConfigHandler()
arch_config = handler.config

assert arch_config.root_enc_password == Password(plaintext='rootPwd')
assert arch_config.auth_config is not None
assert arch_config.auth_config.root_enc_password == Password(plaintext='rootPwd')

assert arch_config.users == [
User(
Expand Down Expand Up @@ -331,7 +332,8 @@ def test_encrypted_creds_with_arg(
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.auth_config is not None
assert arch_config.auth_config.root_enc_password == Password(enc_password='$y$j9T$FWCInXmSsS.8KV4i7O50H.$Hb6/g.Sw1ry888iXgkVgc93YNuVk/Rw94knDKdPVQw7')
assert arch_config.users == [
User(
username='t',
Expand Down Expand Up @@ -359,7 +361,8 @@ def test_encrypted_creds_with_env_var(
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.auth_config is not None
assert arch_config.auth_config.root_enc_password == Password(enc_password='$y$j9T$FWCInXmSsS.8KV4i7O50H.$Hb6/g.Sw1ry888iXgkVgc93YNuVk/Rw94knDKdPVQw7')
assert arch_config.users == [
User(
username='t',
Expand Down