Store password as hash instead of plaintext (#3276)

* Rework user password to be hash

* Update

* Update

* Update

* Update

* Update

* Update

* Update

* Update

* Generate yescrypt hash

* Update

* Update

* Update

* Update
This commit is contained in:
Daniel Girtler 2025-04-08 16:53:18 +10:00 committed by GitHub
parent bae0e29e18
commit 4f1d1b4739
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 369 additions and 187 deletions

View File

@ -19,6 +19,8 @@ depends=(
'e2fsprogs'
'glibc'
'kbd'
'libcrypt.so'
'libxcrypt'
'pciutils'
'procps-ng'
'python'

View File

@ -19,7 +19,7 @@ from archinstall.lib.models.mirrors import MirrorConfiguration
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 User
from archinstall.lib.models.users import Password, User
from archinstall.lib.output import error, warn
from archinstall.lib.plugins import load_plugin
from archinstall.lib.storage import storage
@ -71,16 +71,16 @@ class ArchConfig:
# Special fields that should be handle with care due to security implications
users: list[User] = field(default_factory=list)
disk_encryption: DiskEncryption | None = None
root_password: str | None = None
root_enc_password: Password | None = None
def unsafe_json(self) -> dict[str, Any]:
config = {
'!users': [user.json() for user in self.users],
'!root-password': self.root_password,
'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.disk_encryption:
config['encryption_password'] = self.disk_encryption.encryption_password
if self.disk_encryption and self.disk_encryption.encryption_password:
config['encryption_password'] = self.disk_encryption.encryption_password.plaintext
return config
@ -149,10 +149,12 @@ class ArchConfig:
if net_config := args_config.get('network_config', None):
arch_config.network_config = NetworkConfiguration.parse_arg(net_config)
users = args_config.get('!users', None)
superusers = args_config.get('!superusers', None)
if users is not None or superusers is not None:
arch_config.users = User.parse_arguments(users, superusers)
# DEPRECATED: backwards copatibility
if users := args_config.get('!users', None):
arch_config.users = User.parse_arguments(users)
if users := args_config.get('users', None):
arch_config.users = User.parse_arguments(users)
if bootloader_config := args_config.get('bootloader', None):
arch_config.bootloader = Bootloader.from_arg(bootloader_config)
@ -167,7 +169,7 @@ class ArchConfig:
arch_config.disk_encryption = DiskEncryption.parse_arg(
arch_config.disk_config,
args_config['disk_encryption'],
args_config.get('encryption_password', '')
Password(plaintext=args_config.get('encryption_password', ''))
)
if hostname := args_config.get('hostname', ''):
@ -192,8 +194,12 @@ class ArchConfig:
if services := args_config.get('services', []):
arch_config.services = services
# DEPRECATED: backwards compatibility
if root_password := args_config.get('!root-password', None):
arch_config.root_password = root_password
arch_config.root_enc_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)
if custom_commands := args_config.get('custom_commands', []):
arch_config.custom_commands = custom_commands

72
archinstall/lib/crypt.py Normal file
View File

@ -0,0 +1,72 @@
import ctypes
import ctypes.util
from pathlib import Path
from .output import debug
libcrypt = ctypes.CDLL("libcrypt.so")
libcrypt.crypt.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
libcrypt.crypt.restype = ctypes.c_char_p
libcrypt.crypt_gensalt.argtypes = [ctypes.c_char_p, ctypes.c_ulong, ctypes.c_char_p, ctypes.c_int]
libcrypt.crypt_gensalt.restype = ctypes.c_char_p
LOGIN_DEFS = Path('/etc/login.defs')
def _search_login_defs(key: str) -> str | None:
defs = LOGIN_DEFS.read_text()
for line in defs.split('\n'):
line = line.strip()
if line.startswith('#'):
continue
if line.startswith(key):
value = line.split(' ')[1]
return value
return None
def crypt_gen_salt(prefix: str | bytes, rounds: int) -> bytes:
if isinstance(prefix, str):
prefix = prefix.encode('utf-8')
setting = libcrypt.crypt_gensalt(prefix, rounds, None, 0)
if setting is None:
raise ValueError(f'crypt_gensalt() returned NULL for prefix {prefix!r} and rounds {rounds}')
return setting
def crypt_yescrypt(plaintext: str) -> str:
"""
By default chpasswd in Arch uses PAM to to hash the password with crypt_yescrypt
the PAM code https://github.com/linux-pam/linux-pam/blob/master/modules/pam_unix/support.c
shows that the hashing rounds are determined from YESCRYPT_COST_FACTOR in /etc/login.defs
If no value was specified (or commented out) a default of 5 is choosen
"""
value = _search_login_defs('YESCRYPT_COST_FACTOR')
if value is not None:
rounds = int(value)
if rounds < 3:
rounds = 3
elif rounds > 11:
rounds = 11
else:
rounds = 5
debug(f'Creating yescrypt hash with rounds {rounds}')
enc_plaintext = plaintext.encode('utf-8')
salt = crypt_gen_salt('$y$', rounds)
crypt_hash = libcrypt.crypt(enc_plaintext, salt)
if crypt_hash is None:
raise ValueError('crypt() returned NULL')
return crypt_hash.decode('utf-8')

View File

@ -38,6 +38,7 @@ from ..models.device_model import (
_DeviceInfo,
_PartitionInfo,
)
from ..models.users import Password
from ..output import debug, error, info, log
from ..utils.util import is_subpath
from .utils import (
@ -307,7 +308,7 @@ class DeviceHandler:
self,
dev_path: Path,
mapper_name: str | None,
enc_password: str,
enc_password: Password | None,
lock_after_create: bool = True
) -> Luks2:
luks_handler = Luks2(
@ -338,6 +339,9 @@ class DeviceHandler:
fs_type: FilesystemType,
enc_conf: DiskEncryption
) -> None:
if not enc_conf.encryption_password:
raise ValueError('No encryption password provided')
luks_handler = Luks2(
dev_path,
mapper_name=mapper_name,
@ -678,7 +682,12 @@ class DeviceHandler:
if luks_handler is not None and luks_handler.mapper_dev is not None:
luks_handler.lock()
def unlock_luks2_dev(self, dev_path: Path, mapper_name: str, enc_password: str) -> Luks2:
def unlock_luks2_dev(
self,
dev_path: Path,
mapper_name: str,
enc_password: Password | None
) -> Luks2:
luks_handler = Luks2(dev_path, mapper_name=mapper_name, password=enc_password)
if not luks_handler.is_unlocked():

View File

@ -17,6 +17,7 @@ from archinstall.tui.types import Alignment, FrameProperties, ResultType
from ..menu.abstract_menu import AbstractSubMenu
from ..models.device_model import Fido2Device
from ..models.users import Password
from ..output import FormattedOutput
from ..utils.util import get_password
from .fido import Fido2
@ -122,7 +123,7 @@ class DiskEncryptionMenu(AbstractSubMenu):
super().run()
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
enc_password: str | None = self._item_group.find_by_key('encryption_password').value
enc_password: Password | None = self._item_group.find_by_key('encryption_password').value
enc_partitions = self._item_group.find_by_key('partitions').value
enc_lvm_vols = self._item_group.find_by_key('lvm_volumes').value
@ -183,8 +184,7 @@ class DiskEncryptionMenu(AbstractSubMenu):
enc_pwd = self._item_group.find_by_key('encryption_password').value
if enc_pwd:
pwd_text = '*' * len(enc_pwd)
return f'{_("Encryption password")}: {pwd_text}'
return f'{_("Encryption password")}: {enc_pwd.hidden()}'
return None
@ -249,7 +249,7 @@ def select_encryption_type(disk_config: DiskLayoutConfiguration, preset: Encrypt
return result.get_value()
def select_encrypted_password() -> str | None:
def select_encrypted_password() -> Password | None:
header = str(_('Enter disk encryption password (leave blank for no encryption)')) + '\n'
password = get_password(
text=str(_('Disk encryption password')),

View File

@ -8,6 +8,7 @@ from archinstall.lib.models.device_model import Fido2Device
from ..exceptions import SysCallError
from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes_from_str
from ..models.users import Password
from ..output import error, info
@ -74,7 +75,7 @@ class Fido2:
cls,
hsm_device: Fido2Device,
dev_path: Path,
password: str
password: Password
) -> None:
worker = SysCommandWorker(f"systemd-cryptenroll --fido2-device={hsm_device.path} {dev_path}", peek_output=True)
pw_inputted = False
@ -83,7 +84,7 @@ class Fido2:
while worker.is_alive():
if pw_inputted is False:
if bytes(f"please enter current passphrase for disk {dev_path}", 'UTF-8') in worker._trace_log.lower():
worker.write(bytes(password, 'UTF-8'))
worker.write(bytes(password.plaintext, 'UTF-8'))
pw_inputted = True
elif pin_inputted is False:
if bytes("please enter security token pin", 'UTF-8') in worker._trace_log.lower():

View File

@ -474,8 +474,3 @@ def _pid_exists(pid: int) -> bool:
return any(subprocess.check_output(['ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip())
except subprocess.CalledProcessError:
return False
def secret(x: str) -> str:
""" return * with len equal to to the input string """
return '*' * len(x)

View File

@ -9,7 +9,6 @@ from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from .args import ArchConfig
from .configuration import save_config
from .general import secret
from .hardware import SysInfo
from .interactions.general_conf import (
add_number_of_parallel_downloads,
@ -31,7 +30,7 @@ from .models.locale import LocaleConfiguration
from .models.mirrors import MirrorConfiguration
from .models.network_configuration import NetworkConfiguration, NicType
from .models.profile_model import ProfileConfiguration
from .models.users import User
from .models.users import Password, User
from .output import FormattedOutput
from .translationhandler import Language, translation_handler
from .utils.util import get_password
@ -125,7 +124,7 @@ class GlobalMenu(AbstractMenu):
text=str(_('Root password')),
action=self._set_root_password,
preview_action=self._prev_root_pwd,
key='root_password',
key='root_enc_password',
),
MenuItem(
text=str(_('User account')),
@ -234,8 +233,8 @@ class GlobalMenu(AbstractMenu):
missing = set()
for item in self._item_group.items:
if item.key in ['root_password', 'users']:
if not check('root_password') and not has_superuser():
if item.key in ['root_enc_password', 'users']:
if not check('root_enc_password') and not has_superuser():
missing.add(
str(_('Either root-password or at least 1 user with sudo privileges must be specified'))
)
@ -364,7 +363,8 @@ class GlobalMenu(AbstractMenu):
def _prev_root_pwd(self, item: MenuItem) -> str | None:
if item.value is not None:
return f'{_("Root password")}: {secret(item.value)}'
password: Password = item.value
return f'{_("Root password")}: {password.hidden()}'
return None
def _prev_audio(self, item: MenuItem) -> str | None:
@ -399,7 +399,9 @@ class GlobalMenu(AbstractMenu):
if enc_config:
enc_type = EncryptionType.type_to_text(enc_config.encryption_type)
output = str(_('Encryption type')) + f': {enc_type}\n'
output += str(_('Password')) + f': {secret(enc_config.encryption_password)}\n'
if enc_config.encryption_password:
output += str(_('Password')) + f': {enc_config.encryption_password.hidden()}\n'
if enc_config.partitions:
output += f'Partitions: {len(enc_config.partitions)} selected\n'
@ -501,7 +503,7 @@ class GlobalMenu(AbstractMenu):
return None
def _set_root_password(self, preset: str | None = None) -> str | None:
def _set_root_password(self, preset: str | None = None) -> Password | None:
password = get_password(text=str(_('Root password')), allow_skip=True)
return password

View File

@ -82,7 +82,10 @@ class Installer:
self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S')
self.milliseconds = int(str(time.time()).split('.')[1])
self.helper_flags: dict[str, str | bool | None] = {'base': False, 'bootloader': None}
self._helper_flags: dict[str, str | bool | None] = {
'base': False,
'bootloader': None
}
for kernel in self.kernels:
self._base_packages.append(kernel)
@ -415,11 +418,12 @@ class Installer:
if part_mod.is_root() and not gen_enc_file:
if self._disk_encryption.hsm_device:
Fido2.fido2_enroll(
self._disk_encryption.hsm_device,
part_mod.safe_dev_path,
self._disk_encryption.encryption_password
)
if self._disk_encryption.encryption_password:
Fido2.fido2_enroll(
self._disk_encryption.hsm_device,
part_mod.safe_dev_path,
self._disk_encryption.encryption_password
)
def _generate_key_file_lvm_volumes(self) -> None:
for vol in self._disk_encryption.lvm_volumes:
@ -437,16 +441,17 @@ class Installer:
if vol.is_root() and not gen_enc_file:
if self._disk_encryption.hsm_device:
Fido2.fido2_enroll(
self._disk_encryption.hsm_device,
vol.safe_dev_path,
self._disk_encryption.encryption_password
)
if self._disk_encryption.encryption_password:
Fido2.fido2_enroll(
self._disk_encryption.hsm_device,
vol.safe_dev_path,
self._disk_encryption.encryption_password
)
def sync_log_to_install_medium(self) -> bool:
# Copy over the install log (if there is one) to the install medium if
# at least the base has been strapped in, otherwise we won't have a filesystem/structure to copy to.
if self.helper_flags.get('base-strapped', False) is True:
if self._helper_flags.get('base-strapped', False) is True:
if filename := storage.get('LOG_FILE', None):
absolute_logfile = os.path.join(storage.get('LOG_PATH', './'), filename)
@ -480,7 +485,7 @@ class Installer:
self._kernel_params.append(f'resume_offset={resume_offset}')
def post_install_check(self, *args: str, **kwargs: str) -> list[str]:
return [step for step, flag in self.helper_flags.items() if flag is False]
return [step for step, flag in self._helper_flags.items() if flag is False]
def set_mirrors(
self,
@ -690,7 +695,7 @@ class Installer:
if enable_services:
# If we haven't installed the base yet (function called pre-maturely)
if self.helper_flags.get('base', False) is False:
if self._helper_flags.get('base', False) is False:
self._base_packages.append('iwd')
# This function will be called after minimal_installation()
@ -719,7 +724,7 @@ class Installer:
if enable_services:
# If we haven't installed the base yet (function called pre-maturely)
if self.helper_flags.get('base', False) is False:
if self._helper_flags.get('base', False) is False:
def post_install_enable_networkd_resolved(*args: str, **kwargs: str) -> None:
self.enable_service(['systemd-networkd', 'systemd-resolved'])
@ -847,7 +852,7 @@ class Installer:
pacman_conf.apply()
self.pacman.strap(self._base_packages)
self.helper_flags['base-strapped'] = True
self._helper_flags['base-strapped'] = True
pacman_conf.persist()
@ -878,7 +883,7 @@ class Installer:
if mkinitcpio and not self.mkinitcpio(['-P']):
error('Error generating initramfs (continuing anyway)')
self.helper_flags['base'] = True
self._helper_flags['base'] = True
# Run registered post-install hooks
for function in self.post_base_install:
@ -1144,7 +1149,7 @@ class Installer:
loader_conf.write_text('\n'.join(loader_data) + '\n')
self.helper_flags['bootloader'] = 'systemd'
self._helper_flags['bootloader'] = 'systemd'
def _add_grub_bootloader(
self,
@ -1229,7 +1234,7 @@ class Installer:
except SysCallError as err:
raise DiskError(f"Could not configure GRUB: {err}")
self.helper_flags['bootloader'] = "grub"
self._helper_flags['bootloader'] = "grub"
def _add_limine_bootloader(
self,
@ -1389,7 +1394,7 @@ class Installer:
config_path.write_text(config_contents)
self.helper_flags['bootloader'] = "limine"
self._helper_flags['bootloader'] = "limine"
def _add_efistub_bootloader(
self,
@ -1439,7 +1444,7 @@ class Installer:
cmd = [arg.format(kernel=kernel) for arg in cmd_template]
SysCommand(cmd)
self.helper_flags['bootloader'] = "efistub"
self._helper_flags['bootloader'] = "efistub"
def _config_uki(
self,
@ -1573,12 +1578,9 @@ class Installer:
users = [users]
for user in users:
self.user_create(user.username, user.password, user.groups, user.sudo)
def user_create(self, user: str, password: str | None = None, groups: list[str] | None = None, sudo: bool = False) -> None:
if groups is None:
groups = []
self._create_user(user)
def _create_user(self, user: User) -> None:
# This plugin hook allows for the plugin to handle the creation of the user.
# Password and Group management is still handled by user_create()
handled_by_plugin = False
@ -1588,14 +1590,14 @@ class Installer:
handled_by_plugin = result
if not handled_by_plugin:
info(f'Creating user {user}')
info(f'Creating user {user.username}')
cmd = f'arch-chroot {self.target} useradd -m'
if sudo:
if user.sudo:
cmd += ' -G wheel'
cmd += f' {user}'
cmd += f' {user.username}'
try:
SysCommand(cmd)
@ -1607,29 +1609,29 @@ class Installer:
if result := plugin.on_user_created(self, user):
handled_by_plugin = result
if password:
self.user_set_pw(user, password)
if user.password:
self.set_user_password(user)
if groups:
for group in groups:
SysCommand(f'arch-chroot {self.target} gpasswd -a {user} {group}')
for group in user.groups:
SysCommand(f'arch-chroot {self.target} gpasswd -a {user.username} {group}')
if sudo and self.enable_sudo(user):
self.helper_flags['user'] = True
def set_user_password(self, user: User) -> bool:
info(f'Setting password for {user.username}')
def user_set_pw(self, user: str, password: str) -> bool:
info(f'Setting password for {user}')
enc_password = user.password.enc_password if user.password else None
if user == 'root':
# This means the root account isn't locked/disabled with * in /etc/passwd
self.helper_flags['user'] = True
if not enc_password:
debug('User password is empty')
return False
cmd = ['arch-chroot', str(self.target), 'chpasswd']
input_data = f'{user.username}:{enc_password}'.encode()
cmd = ['arch-chroot', str(self.target), 'chpasswd', '--encrypted']
try:
run(cmd, input_data=f'{user}:{password}'.encode())
run(cmd, input_data=input_data)
return True
except CalledProcessError:
except CalledProcessError as err:
debug(f'Error setting user password: {err}')
return False
def user_set_shell(self, user: str, shell: str) -> bool:

View File

@ -7,7 +7,6 @@ from archinstall.tui.curses_menu import EditMenu, SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.types import Alignment, Orientation, ResultType
from ..general import secret
from ..menu.list_manager import ListManager
from ..models.users import User
from ..utils.util import get_password
@ -91,7 +90,7 @@ class UserList(ListManager):
if not password:
return None
header += f'{_("Password")}: {secret(password)}\n\n'
header += f'{_("Password")}: {password.hidden()}\n\n'
header += str(_('Should "{}" be a superuser (sudo)?\n')).format(username)
group = MenuItemGroup.yes_no()

View File

@ -9,6 +9,7 @@ from archinstall.lib.disk.utils import get_lsblk_info
from .exceptions import DiskError, SysCallError
from .general import SysCommand, SysCommandWorker, generate_password, run
from .models.users import Password
from .output import debug, info
@ -16,7 +17,7 @@ from .output import debug, info
class Luks2:
luks_dev_path: Path
mapper_name: str | None = None
password: str | None = None
password: Password | None = None
key_file: Path | None = None
auto_unmount: bool = False
@ -57,7 +58,7 @@ class Luks2:
if isinstance(self.password, bytes):
return self.password
else:
return bytes(self.password, 'UTF-8')
return bytes(self.password.plaintext, 'UTF-8')
def _get_passphrase_args(
self,

View File

@ -12,6 +12,7 @@ from parted import Disk, Geometry, Partition
from pydantic import BaseModel, Field, ValidationInfo, field_serializer, field_validator
from ..hardware import SysInfo
from ..models.users import Password
from ..output import debug
if TYPE_CHECKING:
@ -1422,7 +1423,7 @@ class _DiskEncryptionSerialization(TypedDict):
@dataclass
class DiskEncryption:
encryption_type: EncryptionType = EncryptionType.NoEncryption
encryption_password: str = ''
encryption_password: Password | None = None
partitions: list[PartitionModification] = field(default_factory=list)
lvm_volumes: list[LvmVolume] = field(default_factory=list)
hsm_device: Fido2Device | None = None
@ -1472,12 +1473,12 @@ class DiskEncryption:
cls,
disk_config: DiskLayoutConfiguration,
disk_encryption: _DiskEncryptionSerialization,
password: str = ''
password: Password | None = None
) -> 'DiskEncryption | None':
if not cls.validate_enc(disk_config):
return None
if len(password) < 1:
if not password:
return None
enc_partitions = []

View File

@ -1,6 +1,8 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, TypedDict, override
from typing import TYPE_CHECKING, NotRequired, TypedDict, override
from ..crypt import crypt_yescrypt
if TYPE_CHECKING:
from collections.abc import Callable
@ -103,72 +105,117 @@ class PasswordStrength(Enum):
return PasswordStrength.VERY_WEAK
_UserSerialization = TypedDict('_UserSerialization', {'username': str, '!password': str, 'sudo': bool})
_UserSerialization = TypedDict(
'_UserSerialization',
{
'username': str,
'!password': NotRequired[str],
'sudo': bool,
'groups': list[str],
'enc_password': str | None
}
)
class Password:
def __init__(
self,
plaintext: str = '',
enc_password: str | None = None
):
if plaintext:
enc_password = crypt_yescrypt(plaintext)
if not plaintext and not enc_password:
raise ValueError('Either plaintext or enc_password must be provided')
self._plaintext = plaintext
self.enc_password = enc_password
@property
def plaintext(self) -> str:
return self._plaintext
@plaintext.setter
def plaintext(self, value: str):
self._plaintext = value
self.enc_password = crypt_yescrypt(value)
@override
def __eq__(self, other: object) -> bool:
if not isinstance(other, Password):
return NotImplemented
if self._plaintext and other._plaintext:
return self._plaintext == other._plaintext
return self.enc_password == other.enc_password
def hidden(self) -> str:
if self._plaintext:
return '*' * len(self._plaintext)
else:
return '*' * 8
@dataclass
class User:
username: str
password: str
password: Password
sudo: bool
groups: list[str] = field(default_factory=list)
@property
def groups(self) -> list[str]:
# this property should be transferred into a class attr instead
# if it's every going to be used
return []
@override
def __str__(self) -> str:
# safety overwrite to make sure password is not leaked
return f'User({self.username=}, {self.sudo=}, {self.groups=})'
def table_data(self) -> dict[str, str | bool | list[str]]:
return {
'username': self.username,
'password': self.password.hidden(),
'sudo': self.sudo,
'groups': self.groups
}
def json(self) -> _UserSerialization:
return {
'username': self.username,
'!password': self.password,
'sudo': self.sudo
'enc_password': self.password.enc_password,
'sudo': self.sudo,
'groups': self.groups
}
@classmethod
def _parse(cls, config_users: list[_UserSerialization]) -> list['User']:
users = []
for entry in config_users:
username = entry.get('username', None)
password = entry.get('!password', '')
sudo = entry.get('sudo', False)
if username is None:
continue
user = User(username, password, sudo)
users.append(user)
return users
@classmethod
def _parse_backwards_compatible(cls, config_users: dict[str, dict[str, str]], sudo: bool) -> list['User']:
if len(config_users.keys()) > 0:
username = list(config_users.keys())[0]
password = config_users[username]['!password']
if password:
return [User(username, password, sudo)]
return []
@classmethod
def parse_arguments(
cls,
config_users: list[_UserSerialization] | dict[str, dict[str, str]],
config_superusers: dict[str, dict[str, str]] | None
args: list[_UserSerialization]
) -> list['User']:
users = []
users: list[User] = []
# backwards compatibility
if isinstance(config_users, dict):
users += cls._parse_backwards_compatible(config_users, False)
else:
users += cls._parse(config_users)
for entry in args:
username = entry.get('username')
password: Password | None = None
groups = entry.get('groups', [])
plaintext = entry.get('!password')
enc_password = entry.get('enc_password')
# backwards compatibility
if isinstance(config_superusers, dict):
users += cls._parse_backwards_compatible(config_superusers, True)
# DEPRECATED: backwards compatibility
if plaintext:
password = Password(plaintext=plaintext)
elif enc_password:
password = Password(enc_password=enc_password)
if username is None or password is None:
continue
user = User(
username=username,
password=password,
sudo=entry.get('sudo', False) is True,
groups=groups
)
users.append(user)
return users

View File

@ -1,9 +0,0 @@
from .profile_menu import ProfileMenu, select_greeter, select_profile
from .profiles_handler import profile_handler
__all__ = [
'ProfileMenu',
'profile_handler',
'select_greeter',
'select_profile',
]

View File

@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
from archinstall.tui.curses_menu import EditMenu
from archinstall.tui.types import Alignment
from ..general import secret
from ..models.users import Password
from ..output import FormattedOutput
if TYPE_CHECKING:
@ -20,7 +20,7 @@ def get_password(
header: str | None = None,
allow_skip: bool = False,
preset: str | None = None
) -> str | None:
) -> Password | None:
failure: str | None = None
while True:
@ -42,13 +42,12 @@ def get_password(
if allow_skip and not result.has_item():
return None
password = result.text()
hidden = secret(password)
password = Password(plaintext=result.text())
if header is not None:
confirmation_header = f'{header}{_("Password")}: {hidden}\n'
confirmation_header = f'{header}{_("Password")}: {password.hidden()}\n'
else:
confirmation_header = f'{_("Password")}: {hidden}\n'
confirmation_header = f'{_("Password")}: {password.hidden()}\n'
result = EditMenu(
str(_('Confirm password')),
@ -58,7 +57,7 @@ def get_password(
hide_input=True
).input()
if password == result.text():
if password._plaintext == result.text():
return password
failure = str(_('The confirmation password did not match, please try again'))

View File

@ -15,6 +15,7 @@ from archinstall.lib.models.device_model import (
EncryptionType,
)
from archinstall.lib.models.network_configuration import NetworkConfiguration
from archinstall.lib.models.users import User
from archinstall.lib.output import debug, error, info
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui import Tui
@ -128,8 +129,9 @@ def perform_installation(mountpoint: Path) -> None:
if accessibility_tools_in_use():
installation.enable_espeakup()
if (root_pw := config.root_password) and len(root_pw):
installation.user_set_pw('root', root_pw)
if root_pw := config.root_enc_password:
root_user = User('root', root_pw, False)
installation.set_user_password(root_user)
if (profile_config := config.profile_config) and profile_config.profile:
profile_config.profile.post_install(installation)

View File

@ -7,14 +7,15 @@ from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.installer import Installer
from archinstall.lib.models import Bootloader, User
from archinstall.lib.models import Bootloader
from archinstall.lib.models.device_model import (
DiskLayoutConfiguration,
)
from archinstall.lib.models.network_configuration import NetworkConfiguration
from archinstall.lib.models.profile_model import ProfileConfiguration
from archinstall.lib.models.users import Password, User
from archinstall.lib.output import debug, error, info
from archinstall.lib.profile import profile_handler
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui import Tui
@ -53,7 +54,7 @@ def perform_installation(mountpoint: Path) -> None:
profile_config = ProfileConfiguration(MinimalProfile())
profile_handler.install_profile_config(installation, profile_config)
user = User('devel', 'devel', False)
user = User('devel', Password(plaintext='devel'), False)
installation.create_users(user)
# Once this is done, we output some useful information to the user

View File

@ -247,7 +247,7 @@ Below is an example of how to set the root password and below that are descripti
.. code-block:: json
{
"!root-password" : "SecretSanta2022"
"root_enc_password" : "SecretSanta2022"
}
.. list-table:: ``--creds`` options
@ -262,16 +262,16 @@ Below is an example of how to set the root password and below that are descripti
- ``str``
- Password to encrypt disk, not encrypted if password not provided
- No
* - ``!root-password``
* - ``root_enc_password``
- ``str``
- The root account password
- No
* - ``!users``
* - ``users``
- .. code-block:: json
{
"username": "<USERNAME>",
"!password": "<PASSWORD>",
"enc_password": "<PASSWORD_HASH>",
"sudo": false
}
- List of regular user credentials, see configuration for reference
@ -280,11 +280,9 @@ Below is an example of how to set the root password and below that are descripti
.. note::
``!users`` is optional only if ``!root-password`` was set. ``!users`` will be enforced otherwise and the minimum amount of users with sudo privileges required will be set to 1.
``users`` is optional only if ``root_enc_password`` was set. ``users`` will be enforced otherwise and the minimum amount of users with sudo privileges required will be set to 1.
.. note::
The keys start with ``!`` because internal log functions will mask any keys starting with exclamation marks from logs and unrestricted configurations.
.. _scripts: https://github.com/archlinux/archinstall/tree/master/archinstall/scripts
.. _Guided Installer: https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py

View File

@ -1,9 +1,11 @@
{
"!users": [
"users": [
{
"sudo": true,
"username": "archinstall"
"username": "archinstall",
"enc_password": "password_hash"
}
],
"encryption_password": "..."
"root_enc_password": "password_hash"
}

View File

@ -19,8 +19,8 @@ from archinstall.lib.models.device_model import (
Unit,
)
from archinstall.lib.models.profile_model import ProfileConfiguration
from archinstall.lib.models.users import User
from archinstall.lib.profile import profile_handler
from archinstall.lib.models.users import Password, User
from archinstall.lib.profile.profiles_handler import profile_handler
# we're creating a new ext4 filesystem installation
fs_type = FilesystemType('ext4')
@ -81,7 +81,7 @@ disk_config = DiskLayoutConfiguration(
# disk encryption configuration (Optional)
disk_encryption = DiskEncryption(
encryption_password="enc_password",
encryption_password=Password(plaintext="enc_password"),
encryption_type=EncryptionType.Luks,
partitions=[home_partition],
hsm_device=None
@ -111,5 +111,5 @@ with Installer(
profile_config = ProfileConfiguration(MinimalProfile())
profile_handler.install_profile_config(installation, profile_config)
user = User('archinstall', 'password', True)
user = User('archinstall', Password(plaintext='password'), True)
installation.create_users(user)

View File

@ -15,6 +15,7 @@ from archinstall.lib.models.device_model import (
EncryptionType,
)
from archinstall.lib.models.network_configuration import NetworkConfiguration
from archinstall.lib.models.users import User
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui import Tui
@ -127,8 +128,9 @@ def perform_installation(mountpoint: Path) -> None:
if accessibility_tools_in_use():
installation.enable_espeakup()
if (root_pw := config.root_password) and len(root_pw):
installation.user_set_pw('root', root_pw)
if root_pw := config.root_enc_password:
root_user = User('root', root_pw, False)
installation.set_user_password(root_user)
if (profile_config := config.profile_config) and profile_config.profile:
profile_config.profile.post_install(installation)

View File

@ -1,7 +1,7 @@
import time
from archinstall.lib.output import info
from archinstall.lib.profile import profile_handler
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.lib.storage import storage
from archinstall.tui import Tui

View File

@ -7,12 +7,13 @@ from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.installer import Installer
from archinstall.lib.models import Bootloader, User
from archinstall.lib.models import Bootloader
from archinstall.lib.models.device_model import DiskLayoutConfiguration
from archinstall.lib.models.network_configuration import NetworkConfiguration
from archinstall.lib.models.profile_model import ProfileConfiguration
from archinstall.lib.models.users import Password, User
from archinstall.lib.output import debug, error, info
from archinstall.lib.profile import profile_handler
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui import Tui
@ -52,7 +53,7 @@ def perform_installation(mountpoint: Path) -> None:
profile_config = ProfileConfiguration(MinimalProfile())
profile_handler.install_profile_config(installation, profile_config)
user = User('devel', 'devel', False)
user = User('devel', Password(plaintext='devel'), False)
installation.create_users(user)
# Once this is done, we output some useful information to the user

View File

@ -14,8 +14,13 @@ def creds_fixture() -> Path:
@pytest.fixture(scope='session')
def mirror_backwards_config() -> Path:
return Path(__file__).parent / 'data' / 'test_config_mirror_backwards.json'
def deprecated_creds_config() -> Path:
return Path(__file__).parent / 'data' / 'test_deprecated_creds_config.json'
@pytest.fixture(scope='session')
def deprecated_mirror_config() -> Path:
return Path(__file__).parent / 'data' / 'test_deprecated_mirror_config.json'
@pytest.fixture(scope='session')

View File

@ -1,10 +1,11 @@
{
"!root-password": "super_pwd",
"!users": [
"root_enc_password": "password_hash",
"users": [
{
"!password": "user_pwd",
"enc_password": "password_hash",
"sudo": true,
"username": "user_name"
"username": "user_name",
"groups": ["wheel"]
}
]
}

View File

@ -0,0 +1,11 @@
{
"!root-password": "rootPwd",
"!users": [
{
"!password": "userPwd",
"sudo": true,
"username": "user_name",
"groups": ["wheel"]
}
]
}

View File

@ -13,7 +13,7 @@ from archinstall.lib.models.mirrors import CustomRepository, CustomServer, Mirro
from archinstall.lib.models.network_configuration import NetworkConfiguration, Nic, NicType
from archinstall.lib.models.packages import Repository
from archinstall.lib.models.profile_model import ProfileConfiguration
from archinstall.lib.models.users import User
from archinstall.lib.models.users import Password, User
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.lib.translationhandler import translation_handler
@ -189,22 +189,29 @@ def test_config_file_parsing(
parallel_downloads=66,
swap=False,
timezone='UTC',
users=[User(username='user_name', password='user_pwd', sudo=True)],
users=[
User(
username='user_name',
password=Password(enc_password='password_hash'),
sudo=True,
groups=['wheel']
)
],
disk_encryption=None,
services=['service_1', 'service_2'],
root_password='super_pwd',
root_enc_password=Password(enc_password='password_hash'),
custom_commands=["echo 'Hello, World!'"]
)
def test_mirror_backwards_config_file_parsing(
def test_deprecated_mirror_config_parsing(
monkeypatch: MonkeyPatch,
mirror_backwards_config: Path,
deprecated_mirror_config: Path,
) -> None:
monkeypatch.setattr('sys.argv', [
'archinstall',
'--config',
str(mirror_backwards_config),
str(deprecated_mirror_config),
])
handler = ArchConfigHandler()
@ -228,3 +235,28 @@ def test_mirror_backwards_config_file_parsing(
)
]
)
def test_deprecated_creds_config_parsing(
monkeypatch: MonkeyPatch,
deprecated_creds_config: Path,
) -> None:
monkeypatch.setattr('sys.argv', [
'archinstall',
'--creds',
str(deprecated_creds_config),
])
handler = ArchConfigHandler()
arch_config = handler.config
assert arch_config.root_enc_password == Password(plaintext='rootPwd')
assert arch_config.users == [
User(
username='user_name',
password=Password(plaintext='userPwd'),
sudo=True,
groups=['wheel']
)
]