Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ depends=(
'e2fsprogs'
'glibc'
'kbd'
'libcrypt.so'
'libxcrypt'
'pciutils'
'procps-ng'
'python'
Expand Down
30 changes: 18 additions & 12 deletions archinstall/lib/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,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 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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -149,10 +149,12 @@ def from_config(cls, args_config: dict[str, Any]) -> '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)
Expand All @@ -167,7 +169,7 @@ def from_config(cls, args_config: dict[str, Any]) -> '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', ''):
Expand All @@ -192,8 +194,12 @@ def from_config(cls, args_config: dict[str, Any]) -> '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
Expand Down
72 changes: 72 additions & 0 deletions archinstall/lib/crypt.py
Original file line number Diff line number Diff line change
@@ -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')
13 changes: 11 additions & 2 deletions archinstall/lib/disk/device_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
_DeviceInfo,
_PartitionInfo,
)
from ..models.users import Password
from ..output import debug, error, info, log
from ..utils.util import is_subpath
from .utils import (
Expand Down Expand Up @@ -307,7 +308,7 @@ def encrypt(
self,
dev_path: Path,
mapper_name: str | None,
enc_password: str,
enc_password: Password | None,
lock_after_create: bool = True
) -> Luks2:
luks_handler = Luks2(
Expand Down Expand Up @@ -338,6 +339,9 @@ def format_encrypted(
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,
Expand Down Expand Up @@ -678,7 +682,12 @@ def create_btrfs_volumes(
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():
Expand Down
8 changes: 4 additions & 4 deletions archinstall/lib/disk/encryption_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

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
Expand Down Expand Up @@ -122,7 +123,7 @@ def run(self) -> DiskEncryption | None:
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

Expand Down Expand Up @@ -183,8 +184,7 @@ def _prev_password(self) -> str | None:
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

Expand Down Expand Up @@ -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')),
Expand Down
5 changes: 3 additions & 2 deletions archinstall/lib/disk/fido.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

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


Expand Down Expand Up @@ -74,7 +75,7 @@ def fido2_enroll(
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
Expand All @@ -83,7 +84,7 @@ def fido2_enroll(
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():
Expand Down
5 changes: 0 additions & 5 deletions archinstall/lib/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
18 changes: 10 additions & 8 deletions archinstall/lib/global_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

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,
Expand All @@ -31,7 +30,7 @@
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
Expand Down Expand Up @@ -125,7 +124,7 @@ def _get_menu_options(self) -> list[MenuItem]:
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')),
Expand Down Expand Up @@ -234,8 +233,8 @@ def has_superuser() -> bool:
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'))
)
Expand Down Expand Up @@ -364,7 +363,8 @@ def _prev_hostname(self, item: MenuItem) -> str | None:

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:
Expand Down Expand Up @@ -399,7 +399,9 @@ def _prev_disk_encryption(self, item: MenuItem) -> str | None:
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'
Expand Down Expand Up @@ -501,7 +503,7 @@ def _prev_profile(self, item: MenuItem) -> str | None:

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

Expand Down
Loading
Loading