|
| 1 | +import getpass |
| 2 | +from pathlib import Path |
| 3 | +from typing import TYPE_CHECKING |
| 4 | + |
| 5 | +from archinstall.lib.general import SysCommandWorker |
| 6 | +from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod |
| 7 | +from archinstall.lib.models.users import User |
| 8 | +from archinstall.lib.output import debug |
| 9 | +from archinstall.lib.translationhandler import tr |
| 10 | +from archinstall.tui.curses_menu import Tui |
| 11 | + |
| 12 | +if TYPE_CHECKING: |
| 13 | + from archinstall.lib.installer import Installer |
| 14 | + |
| 15 | + |
| 16 | +class AuthenticationHandler: |
| 17 | + def __init__(self) -> None: |
| 18 | + self._u2f_auth_file = Path('etc/u2f_mappings') |
| 19 | + |
| 20 | + def setup_auth( |
| 21 | + self, |
| 22 | + install_session: 'Installer', |
| 23 | + auth_config: AuthenticationConfiguration, |
| 24 | + users: list['User'], |
| 25 | + hostname: str, |
| 26 | + ) -> None: |
| 27 | + if auth_config.u2f_config and users is not None: |
| 28 | + self._setup_u2f_login(install_session, auth_config.u2f_config, users, hostname) |
| 29 | + |
| 30 | + def _setup_u2f_login(self, install_session: 'Installer', u2f_config: U2FLoginConfiguration, users: list[User], hostname: str) -> None: |
| 31 | + self._configure_u2f_mapping(install_session, u2f_config, users, hostname) |
| 32 | + self._update_pam_config(install_session, u2f_config) |
| 33 | + |
| 34 | + def _update_pam_config( |
| 35 | + self, |
| 36 | + install_session: 'Installer', |
| 37 | + u2f_config: U2FLoginConfiguration, |
| 38 | + ) -> None: |
| 39 | + match u2f_config.u2f_login_method: |
| 40 | + case U2FLoginMethod.Passwordless: |
| 41 | + config_entry = 'auth sufficient pam_u2f.so authfile=/etc/u2f_mappings cue' |
| 42 | + case U2FLoginMethod.SecondFactor: |
| 43 | + config_entry = 'auth required pam_u2f.so authfile=/etc/u2f_mappings cue' |
| 44 | + case _: |
| 45 | + raise ValueError(f'Unknown U2F login method: {u2f_config.u2f_login_method}') |
| 46 | + |
| 47 | + debug(f'U2F PAM configuration: {config_entry}') |
| 48 | + debug(f'Passwordless sudo enabled: {u2f_config.passwordless_sudo}') |
| 49 | + |
| 50 | + sudo_config = install_session.target / 'etc/pam.d/sudo' |
| 51 | + sys_login = install_session.target / 'etc/pam.d/system-login' |
| 52 | + |
| 53 | + if u2f_config.passwordless_sudo: |
| 54 | + self._add_u2f_entry(sudo_config, config_entry) |
| 55 | + |
| 56 | + self._add_u2f_entry(sys_login, config_entry) |
| 57 | + |
| 58 | + def _add_u2f_entry(self, file: Path, entry: str) -> None: |
| 59 | + if not file.exists(): |
| 60 | + debug(f'File does not exist: {file}') |
| 61 | + return None |
| 62 | + |
| 63 | + content = file.read_text().splitlines() |
| 64 | + |
| 65 | + # remove any existing u2f auth entry |
| 66 | + content = [line for line in content if 'pam_u2f.so' not in line] |
| 67 | + |
| 68 | + # add the u2f auth entry as the first one after comments |
| 69 | + for i, line in enumerate(content): |
| 70 | + if not line.startswith('#'): |
| 71 | + content.insert(i, entry) |
| 72 | + break |
| 73 | + else: |
| 74 | + content.append(entry) |
| 75 | + |
| 76 | + file.write_text('\n'.join(content) + '\n') |
| 77 | + |
| 78 | + def _configure_u2f_mapping( |
| 79 | + self, |
| 80 | + install_session: 'Installer', |
| 81 | + u2f_config: U2FLoginConfiguration, |
| 82 | + users: list[User], |
| 83 | + hostname: str, |
| 84 | + ) -> None: |
| 85 | + debug(f'Setting up U2F login: {u2f_config.u2f_login_method.value}') |
| 86 | + |
| 87 | + install_session.pacman.strap('pam-u2f') |
| 88 | + |
| 89 | + Tui.print(tr(f'Setting up U2F login: {u2f_config.u2f_login_method.value}')) |
| 90 | + |
| 91 | + # https://developers.yubico.com/pam-u2f/ |
| 92 | + u2f_auth_file = install_session.target / 'etc/u2f_mappings' |
| 93 | + u2f_auth_file.touch() |
| 94 | + existing_keys = u2f_auth_file.read_text() |
| 95 | + |
| 96 | + registered_keys: list[str] = [] |
| 97 | + |
| 98 | + for user in users: |
| 99 | + Tui.print('') |
| 100 | + Tui.print(tr('Setting up U2F device for user: {}').format(user.username)) |
| 101 | + Tui.print(tr('You may need to enter the PIN and then touch your U2F device to register it')) |
| 102 | + |
| 103 | + cmd = ' '.join(['arch-chroot', str(install_session.target), 'pamu2fcfg', '-u', user.username, '-o', f'pam://{hostname}', '-i', f'pam://{hostname}']) |
| 104 | + |
| 105 | + debug(f'Enrolling U2F device: {cmd}') |
| 106 | + |
| 107 | + worker = SysCommandWorker(cmd, peek_output=True) |
| 108 | + pin_inputted = False |
| 109 | + |
| 110 | + while worker.is_alive(): |
| 111 | + if pin_inputted is False: |
| 112 | + if bytes('enter pin for', 'UTF-8') in worker._trace_log.lower(): |
| 113 | + worker.write(bytes(getpass.getpass(''), 'UTF-8')) |
| 114 | + pin_inputted = True |
| 115 | + |
| 116 | + output = worker.decode().strip().splitlines() |
| 117 | + debug(f'Output from pamu2fcfg: {output}') |
| 118 | + |
| 119 | + key = output[-1].strip() |
| 120 | + registered_keys.append(key) |
| 121 | + |
| 122 | + all_keys = '\n'.join(registered_keys) |
| 123 | + |
| 124 | + if existing_keys: |
| 125 | + existing_keys += f'\n{all_keys}' |
| 126 | + else: |
| 127 | + existing_keys = all_keys |
| 128 | + |
| 129 | + u2f_auth_file.write_text(existing_keys) |
| 130 | + |
| 131 | + |
| 132 | +auth_handler = AuthenticationHandler() |
0 commit comments