From 0c5ec83b25f5e6892a0a1cbd80d286fc54e48ffc Mon Sep 17 00:00:00 2001 From: Alperen42v <243298691+Alperen42v@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:21:09 +0300 Subject: [PATCH 1/8] feat(disk): fix menu rendering and complete backend data pipeline for custom LUKS2 cipher --- archinstall/lib/disk/device_handler.py | 7 ++- archinstall/lib/disk/encryption_menu.py | 80 +++++++++++++++++++++++++ archinstall/lib/disk/luks.py | 36 +++++++++-- archinstall/lib/models/device.py | 41 +++++++++++++ 4 files changed, 157 insertions(+), 7 deletions(-) diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 360cbfd16a..4b0e40b928 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -17,6 +17,7 @@ from archinstall.lib.exceptions import DiskError, SysCallError, UnknownFilesystemFormat from archinstall.lib.log import debug, error, info, log from archinstall.lib.models.device import ( + DEFAULT_CIPHER, DEFAULT_ITER_TIME, BDevice, BtrfsMountOption, @@ -280,6 +281,8 @@ def encrypt( enc_password: Password | None, lock_after_create: bool = True, iter_time: int = DEFAULT_ITER_TIME, + cipher: str = DEFAULT_CIPHER, + integrity: str | None = None, ) -> Luks2: luks_handler = Luks2( dev_path, @@ -287,7 +290,7 @@ def encrypt( password=enc_password, ) - key_file = luks_handler.encrypt(iter_time=iter_time) + key_file = luks_handler.encrypt(iter_time=iter_time, cipher=cipher, integrity=integrity) udev_sync() @@ -318,7 +321,7 @@ def format_encrypted( password=enc_conf.encryption_password, ) - key_file = luks_handler.encrypt(iter_time=enc_conf.iter_time) + key_file = luks_handler.encrypt(iter_time=enc_conf.iter_time, cipher=enc_conf.cipher, integrity=enc_conf.integrity) udev_sync() diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 53aa73fe37..8153b35e5e 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -7,6 +7,8 @@ from archinstall.lib.menu.menu_helper import MenuHelper from archinstall.lib.menu.util import get_password from archinstall.lib.models.device import ( + AEAD_CIPHERS, + DEFAULT_CIPHER, DEFAULT_ITER_TIME, DeviceModification, DiskEncryption, @@ -64,6 +66,14 @@ def _define_menu_options(self) -> list[MenuItem]: preview_action=self._prev_password, key='encryption_password', ), + MenuItem( + text=tr('Encryption cipher'), + action=self._select_cipher, + value=self._enc_config.cipher, + dependencies=[self._check_dep_enc_type], + preview_action=self._prev_cipher, + key='cipher', + ), MenuItem( text=tr('Iteration time'), action=select_iteration_time, @@ -103,6 +113,19 @@ async def _select_lvm_vols(self, preset: list[LvmVolume]) -> list[LvmVolume]: return await select_lvm_vols_to_encrypt(self._lvm_config, preset=preset) return [] + async def _select_cipher(self, preset: str | None) -> str | None: + cipher = await select_encryption_cipher(preset) + + # AEAD ciphers (e.g. chacha20-random) require LUKS2 + --integrity; + # keep the underlying DiskEncryption.integrity in sync automatically + # so the user never has to set it manually or risk an invalid combo. + if cipher in AEAD_CIPHERS: + self._enc_config.integrity = AEAD_CIPHERS[cipher] + else: + self._enc_config.integrity = None + + return cipher + def _check_dep_enc_type(self) -> bool: enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value if enc_type and enc_type != EncryptionType.NO_ENCRYPTION: @@ -129,6 +152,7 @@ async def show(self) -> DiskEncryption | None: enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value enc_password: Password | None = self._item_group.find_by_key('encryption_password').value + cipher: str | None = self._item_group.find_by_key('cipher').value iter_time: int | None = self._item_group.find_by_key('iter_time').value enc_partitions = self._item_group.find_by_key('partitions').value enc_lvm_vols = self._item_group.find_by_key('lvm_volumes').value @@ -144,6 +168,9 @@ async def show(self) -> DiskEncryption | None: enc_partitions = [] if enc_type != EncryptionType.NO_ENCRYPTION and enc_password and (enc_partitions or enc_lvm_vols): + cipher = cipher or DEFAULT_CIPHER + integrity = AEAD_CIPHERS.get(cipher) + return DiskEncryption( encryption_password=enc_password, encryption_type=enc_type, @@ -151,6 +178,8 @@ async def show(self) -> DiskEncryption | None: lvm_volumes=enc_lvm_vols, hsm_device=enc_config.hsm_device, iter_time=iter_time or DEFAULT_ITER_TIME, + cipher=cipher, + integrity=integrity, ) return None @@ -164,6 +193,9 @@ def _preview(self, item: MenuItem) -> str | None: if (enc_pwd := self._prev_password(item)) is not None: output += f'\n{enc_pwd}' + if (cipher := self._prev_cipher(item)) is not None: + output += f'\n{cipher}' + if (iter_time := self._prev_iter_time(item)) is not None: output += f'\n{iter_time}' @@ -196,6 +228,18 @@ def _prev_password(self, item: MenuItem) -> str | None: return None + def _prev_cipher(self, item: MenuItem) -> str | None: + cipher = self._item_group.find_by_key('cipher').value + if cipher: + output = f'{tr("Encryption cipher")}: {cipher}' + + if cipher in AEAD_CIPHERS: + integrity = AEAD_CIPHERS[cipher] + output += f'\n{tr("Integrity")}: {integrity} ({tr("requires LUKS2")})' + + return output + return None + def _prev_partitions(self, item: MenuItem) -> str | None: if item.value: output = tr('Partitions to be encrypted') + '\n' @@ -404,3 +448,39 @@ def validate_iter_time(value: str) -> str | None: return int(result.get_value()) case ResultType.Reset: return None + + +async def select_encryption_cipher(preset: str | None = None) -> str | None: + # Regular block-cipher modes: accepted directly by `cryptsetup --cipher`. + # AEAD modes (see AEAD_CIPHERS in models/device.py) need LUKS2 and a + # separate --integrity value, which DiskEncryption.__post_init__ and + # DiskEncryptionMenu._select_cipher fill in automatically. + options = [ + 'aes-xts-plain64', + 'aes-cbc-essiv:sha256', + 'serpent-xts-plain64', + 'twofish-xts-plain64', + 'chacha20-random', # AEAD, requires --integrity poly1305, LUKS2 only + ] + + if not preset: + preset = options[0] + + items = [MenuItem(o, value=o) for o in options] + group = MenuItemGroup(items) + group.set_focus_by_value(preset) + + result = await Selection[str]( + group, + header=tr('Select encryption cipher'), + allow_skip=True, + allow_reset=True, + ).show() + + match result.type_: + case ResultType.Reset: + return None + case ResultType.Skip: + return preset + case ResultType.Selection: + return result.get_value() diff --git a/archinstall/lib/disk/luks.py b/archinstall/lib/disk/luks.py index 3a3679d0b2..06bf00037b 100644 --- a/archinstall/lib/disk/luks.py +++ b/archinstall/lib/disk/luks.py @@ -3,12 +3,13 @@ from pathlib import Path from subprocess import CalledProcessError from types import TracebackType +from typing import cast from archinstall.lib.command import SysCommand, SysCommandWorker, run from archinstall.lib.disk.utils import get_lsblk_info, umount from archinstall.lib.exceptions import DiskError, SysCallError from archinstall.lib.log import debug, info -from archinstall.lib.models.device import DEFAULT_ITER_TIME +from archinstall.lib.models.device import AEAD_CIPHERS, DEFAULT_CIPHER, DEFAULT_ITER_TIME from archinstall.lib.models.users import Password from archinstall.lib.utils.util import generate_password @@ -73,11 +74,21 @@ def encrypt( hash_type: str = 'sha512', iter_time: int = DEFAULT_ITER_TIME, key_file: Path | None = None, + cipher: str = DEFAULT_CIPHER, + integrity: str | None = None, ) -> Path | None: debug(f'Luks2 encrypting: {self.luks_dev_path}') key_file_arg, passphrase = self._get_passphrase_args(key_file) + is_aead = cipher in AEAD_CIPHERS + + if is_aead and not integrity: + integrity = AEAD_CIPHERS[cipher] + + if integrity and not is_aead: + raise DiskError(f'Integrity option "{integrity}" is only valid for AEAD ciphers, not "{cipher}"') + cmd = [ 'cryptsetup', '--batch-mode', @@ -86,10 +97,25 @@ def encrypt( 'luks2', '--pbkdf', 'argon2id', - '--hash', - hash_type, - '--key-size', - str(key_size), + '--cipher', + cipher, + ] + + if is_aead: + # AEAD ciphers (e.g. chacha20-random) authenticate each sector via + # --integrity and do not take --hash or a fixed --key-size the same + # way length-preserving modes (aes-xts-plain64 etc.) do; key size is + # derived from the cipher/integrity combination by cryptsetup itself. + cmd += ['--integrity', cast(str, integrity)] + else: + cmd += [ + '--hash', + hash_type, + '--key-size', + str(key_size), + ] + + cmd += [ '--iter-time', str(iter_time), *key_file_arg, diff --git a/archinstall/lib/models/device.py b/archinstall/lib/models/device.py index a50f7d8e62..d0b14ba937 100644 --- a/archinstall/lib/models/device.py +++ b/archinstall/lib/models/device.py @@ -1462,12 +1462,26 @@ def type_to_text(self) -> str: return type_to_text[self] +DEFAULT_CIPHER = 'aes-xts-plain64' + +# AEAD (Authenticated Encryption with Associated Data) ciphers require LUKS2 +# and a separate --integrity parameter; they cannot be used like normal +# block-cipher + chainmode + ivmode strings (e.g. aes-xts-plain64). +# Mapping: cipher value used in --cipher -> required --integrity value +AEAD_CIPHERS: dict[str, str] = { + 'chacha20-random': 'poly1305', + 'aes-gcm-random': 'aead', +} + + class _DiskEncryptionSerialization(TypedDict): encryption_type: str partitions: list[str] lvm_volumes: list[str] hsm_device: NotRequired[_Fido2DeviceSerialization] iter_time: NotRequired[int] + cipher: NotRequired[str] + integrity: NotRequired[str] @dataclass @@ -1478,6 +1492,8 @@ class DiskEncryption: lvm_volumes: list[LvmVolume] = field(default_factory=list) hsm_device: Fido2Device | None = None iter_time: int = DEFAULT_ITER_TIME + cipher: str = DEFAULT_CIPHER + integrity: str | None = None def __post_init__(self) -> None: if self.encryption_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS] and not self.partitions: @@ -1486,6 +1502,20 @@ def __post_init__(self) -> None: if self.encryption_type == EncryptionType.LUKS_ON_LVM and not self.lvm_volumes: raise ValueError('LuksOnLvm encryption require LMV volumes to be defined') + # AEAD ciphers (chacha20-random, aes-gcm-random) are only valid on LUKS2 + # and require --integrity to be set; auto-fill it if missing so the + # cryptsetup call downstream doesn't end up with an invalid combination. + if self.cipher in AEAD_CIPHERS: + required_integrity = AEAD_CIPHERS[self.cipher] + if not self.integrity: + self.integrity = required_integrity + elif self.integrity != required_integrity: + raise ValueError( + f'Cipher {self.cipher!r} requires integrity {required_integrity!r}, got {self.integrity!r}' + ) + elif self.integrity: + raise ValueError(f'Integrity option is only valid for AEAD ciphers, not {self.cipher!r}') + def should_generate_encryption_file(self, dev: PartitionModification | LvmVolume) -> bool: if isinstance(dev, PartitionModification): return dev in self.partitions and dev.mountpoint != Path('/') @@ -1505,6 +1535,12 @@ def json(self) -> _DiskEncryptionSerialization: if self.iter_time != DEFAULT_ITER_TIME: # Only include if not default obj['iter_time'] = self.iter_time + if self.cipher != DEFAULT_CIPHER: # Only include if not default + obj['cipher'] = self.cipher + + if self.integrity: + obj['integrity'] = self.integrity + return obj @staticmethod @@ -1549,11 +1585,16 @@ def parse_arg( if vol.obj_id in disk_encryption.get('lvm_volumes', []): volumes.append(vol) + cipher = disk_encryption.get('cipher', None) or DEFAULT_CIPHER + integrity = disk_encryption.get('integrity', None) + enc = cls( EncryptionType(disk_encryption['encryption_type']), password, enc_partitions, volumes, + cipher=cipher, + integrity=integrity, ) if hsm := disk_encryption.get('hsm_device', None): From 8eec5a731a00c618a4bf753617020afb7b2df7df Mon Sep 17 00:00:00 2001 From: Alperen42v <243298691+Alperen42v@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:50:40 +0300 Subject: [PATCH 2/8] fix : removed chacha20 --- archinstall/lib/disk/device_handler.py | 4 ++- archinstall/lib/disk/encryption_menu.py | 3 +- archinstall/lib/disk/luks.py | 48 +++++++++++++++++++++++-- archinstall/lib/models/device.py | 3 +- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 4b0e40b928..d86ab062e5 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -333,6 +333,8 @@ def format_encrypted( info(f'luks2 formatting mapper dev: {luks_handler.mapper_dev}') self.format(fs_type, luks_handler.mapper_dev) + udev_sync() + info(f'luks2 locking device: {dev_path}') luks_handler.lock() @@ -626,4 +628,4 @@ def wipe_dev(self, block_device: BDevice) -> None: self._wipe(block_device.device_info.path) -device_handler = DeviceHandler() +device_handler = DeviceHandler() \ No newline at end of file diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 8153b35e5e..3c0cd059ef 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -460,7 +460,6 @@ async def select_encryption_cipher(preset: str | None = None) -> str | None: 'aes-cbc-essiv:sha256', 'serpent-xts-plain64', 'twofish-xts-plain64', - 'chacha20-random', # AEAD, requires --integrity poly1305, LUKS2 only ] if not preset: @@ -483,4 +482,4 @@ async def select_encryption_cipher(preset: str | None = None) -> str | None: case ResultType.Skip: return preset case ResultType.Selection: - return result.get_value() + return result.get_value() \ No newline at end of file diff --git a/archinstall/lib/disk/luks.py b/archinstall/lib/disk/luks.py index 06bf00037b..8788262e4f 100644 --- a/archinstall/lib/disk/luks.py +++ b/archinstall/lib/disk/luks.py @@ -163,6 +163,18 @@ def unlock(self, key_file: Path | None = None) -> None: if not self.mapper_name: raise ValueError('mapper name missing') + # If a mapper device with this name already exists (e.g. left over from a + # previous failed run), close it before trying to open a new one. + # cryptsetup open returns exit code 5 / "Device already exists" otherwise. + if self.is_unlocked(): + debug(f'Mapper {self.mapper_name} already open, closing before re-opening') + try: + SysCommand(f'cryptsetup close {self.mapper_name}') + except SysCallError as close_err: + raise DiskError( + f'Could not close existing mapper "{self.mapper_name}" before unlock: {close_err}' + ) + key_file_arg, passphrase = self._get_passphrase_args(key_file) cmd = [ @@ -187,6 +199,7 @@ def unlock(self, key_file: Path | None = None) -> None: raise DiskError(f'Failed to open luks2 device: {self.luks_dev_path}') def lock(self) -> None: + import time umount(self.luks_dev_path) # Get crypt-information about the device by doing a reverse lookup starting with the partition path @@ -195,14 +208,43 @@ def lock(self) -> None: # For each child (sub-partition/sub-device) for child in lsblk_info.children: - # Unmount the child location for mountpoint in child.mountpoints: debug(f'Unmounting {mountpoint}') umount(mountpoint, recursive=True) + # Wait for udev to finish processing events so the kernel drops + # any lingering reference on the mapper device before we close it. + try: + run(['udevadm', 'settle', '--timeout=5']) + except Exception: + pass + # And close it if possible. debug(f'Closing crypt device {child.name}') - SysCommand(f'cryptsetup close {child.name}') + + mapper_dev = Path(f'/dev/mapper/{child.name}') + try: + SysCommand(f'cryptsetup close {child.name}') + except SysCallError as err: + debug(f'cryptsetup close failed ({err}), retrying with --deferred') + try: + SysCommand(f'cryptsetup close --deferred {child.name}') + debug(f'cryptsetup close --deferred issued for {child.name}') + except SysCallError as deferred_err: + raise DiskError( + f'Could not close luks2 device "{child.name}": {deferred_err}' + ) from deferred_err + + # Wait until the mapper device node actually disappears before returning. + # Subsequent commands (wipefs, mkfs, etc.) will fail with "Device busy" + # if we return while the node still exists. + for _ in range(15): + if not mapper_dev.exists(): + break + debug(f'Waiting for {mapper_dev} to disappear...') + time.sleep(1) + else: + raise DiskError(f'Mapper device {mapper_dev} did not disappear after close') def create_keyfile(self, target_path: Path, override: bool = False) -> None: """ @@ -286,4 +328,4 @@ def unlock_luks2_dev( if not luks_handler.is_unlocked(): luks_handler.unlock() - return luks_handler + return luks_handler \ No newline at end of file diff --git a/archinstall/lib/models/device.py b/archinstall/lib/models/device.py index d0b14ba937..50d8206810 100644 --- a/archinstall/lib/models/device.py +++ b/archinstall/lib/models/device.py @@ -1469,7 +1469,6 @@ def type_to_text(self) -> str: # block-cipher + chainmode + ivmode strings (e.g. aes-xts-plain64). # Mapping: cipher value used in --cipher -> required --integrity value AEAD_CIPHERS: dict[str, str] = { - 'chacha20-random': 'poly1305', 'aes-gcm-random': 'aead', } @@ -1686,4 +1685,4 @@ def serialize_size(self, size: Size) -> str: @classmethod def fields(cls) -> list[str]: - return [field.alias or name for name, field in cls.model_fields.items() if name != 'children'] + return [field.alias or name for name, field in cls.model_fields.items() if name != 'children'] \ No newline at end of file From 228a86c474bc019307ecaa95a135fa4d7ac4ef2a Mon Sep 17 00:00:00 2001 From: Alperen42v <243298691+Alperen42v@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:23:54 +0300 Subject: [PATCH 3/8] fix the problems for filesystems --- archinstall/lib/disk/device_handler.py | 5 ++-- archinstall/lib/disk/encryption_menu.py | 30 +++------------------ archinstall/lib/disk/luks.py | 33 +++++++---------------- archinstall/lib/models/device.py | 35 +++++-------------------- 4 files changed, 21 insertions(+), 82 deletions(-) diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index d86ab062e5..f2f7908896 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -282,7 +282,6 @@ def encrypt( lock_after_create: bool = True, iter_time: int = DEFAULT_ITER_TIME, cipher: str = DEFAULT_CIPHER, - integrity: str | None = None, ) -> Luks2: luks_handler = Luks2( dev_path, @@ -290,7 +289,7 @@ def encrypt( password=enc_password, ) - key_file = luks_handler.encrypt(iter_time=iter_time, cipher=cipher, integrity=integrity) + key_file = luks_handler.encrypt(iter_time=iter_time, cipher=cipher) udev_sync() @@ -321,7 +320,7 @@ def format_encrypted( password=enc_conf.encryption_password, ) - key_file = luks_handler.encrypt(iter_time=enc_conf.iter_time, cipher=enc_conf.cipher, integrity=enc_conf.integrity) + key_file = luks_handler.encrypt(iter_time=enc_conf.iter_time, cipher=enc_conf.cipher) udev_sync() diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 3c0cd059ef..30ae1a7ff8 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -7,7 +7,6 @@ from archinstall.lib.menu.menu_helper import MenuHelper from archinstall.lib.menu.util import get_password from archinstall.lib.models.device import ( - AEAD_CIPHERS, DEFAULT_CIPHER, DEFAULT_ITER_TIME, DeviceModification, @@ -114,17 +113,7 @@ async def _select_lvm_vols(self, preset: list[LvmVolume]) -> list[LvmVolume]: return [] async def _select_cipher(self, preset: str | None) -> str | None: - cipher = await select_encryption_cipher(preset) - - # AEAD ciphers (e.g. chacha20-random) require LUKS2 + --integrity; - # keep the underlying DiskEncryption.integrity in sync automatically - # so the user never has to set it manually or risk an invalid combo. - if cipher in AEAD_CIPHERS: - self._enc_config.integrity = AEAD_CIPHERS[cipher] - else: - self._enc_config.integrity = None - - return cipher + return await select_encryption_cipher(preset) def _check_dep_enc_type(self) -> bool: enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value @@ -168,9 +157,6 @@ async def show(self) -> DiskEncryption | None: enc_partitions = [] if enc_type != EncryptionType.NO_ENCRYPTION and enc_password and (enc_partitions or enc_lvm_vols): - cipher = cipher or DEFAULT_CIPHER - integrity = AEAD_CIPHERS.get(cipher) - return DiskEncryption( encryption_password=enc_password, encryption_type=enc_type, @@ -178,8 +164,7 @@ async def show(self) -> DiskEncryption | None: lvm_volumes=enc_lvm_vols, hsm_device=enc_config.hsm_device, iter_time=iter_time or DEFAULT_ITER_TIME, - cipher=cipher, - integrity=integrity, + cipher=cipher or DEFAULT_CIPHER, ) return None @@ -231,13 +216,7 @@ def _prev_password(self, item: MenuItem) -> str | None: def _prev_cipher(self, item: MenuItem) -> str | None: cipher = self._item_group.find_by_key('cipher').value if cipher: - output = f'{tr("Encryption cipher")}: {cipher}' - - if cipher in AEAD_CIPHERS: - integrity = AEAD_CIPHERS[cipher] - output += f'\n{tr("Integrity")}: {integrity} ({tr("requires LUKS2")})' - - return output + return f'{tr("Encryption cipher")}: {cipher}' return None def _prev_partitions(self, item: MenuItem) -> str | None: @@ -452,9 +431,6 @@ def validate_iter_time(value: str) -> str | None: async def select_encryption_cipher(preset: str | None = None) -> str | None: # Regular block-cipher modes: accepted directly by `cryptsetup --cipher`. - # AEAD modes (see AEAD_CIPHERS in models/device.py) need LUKS2 and a - # separate --integrity value, which DiskEncryption.__post_init__ and - # DiskEncryptionMenu._select_cipher fill in automatically. options = [ 'aes-xts-plain64', 'aes-cbc-essiv:sha256', diff --git a/archinstall/lib/disk/luks.py b/archinstall/lib/disk/luks.py index 8788262e4f..ff2e529b47 100644 --- a/archinstall/lib/disk/luks.py +++ b/archinstall/lib/disk/luks.py @@ -3,13 +3,12 @@ from pathlib import Path from subprocess import CalledProcessError from types import TracebackType -from typing import cast from archinstall.lib.command import SysCommand, SysCommandWorker, run from archinstall.lib.disk.utils import get_lsblk_info, umount from archinstall.lib.exceptions import DiskError, SysCallError from archinstall.lib.log import debug, info -from archinstall.lib.models.device import AEAD_CIPHERS, DEFAULT_CIPHER, DEFAULT_ITER_TIME +from archinstall.lib.models.device import CIPHER_KEY_SIZES, DEFAULT_CIPHER, DEFAULT_ITER_TIME from archinstall.lib.models.users import Password from archinstall.lib.utils.util import generate_password @@ -75,19 +74,15 @@ def encrypt( iter_time: int = DEFAULT_ITER_TIME, key_file: Path | None = None, cipher: str = DEFAULT_CIPHER, - integrity: str | None = None, ) -> Path | None: debug(f'Luks2 encrypting: {self.luks_dev_path}') key_file_arg, passphrase = self._get_passphrase_args(key_file) - is_aead = cipher in AEAD_CIPHERS - - if is_aead and not integrity: - integrity = AEAD_CIPHERS[cipher] - - if integrity and not is_aead: - raise DiskError(f'Integrity option "{integrity}" is only valid for AEAD ciphers, not "{cipher}"') + # Resolve the correct key size for this cipher. + # XTS mode uses two keys (e.g. 256+256 = 512 bits for aes-xts-plain64), + # CBC mode uses a single key (max 256 bits for aes-cbc-essiv:sha256). + resolved_key_size = CIPHER_KEY_SIZES.get(cipher, key_size) cmd = [ 'cryptsetup', @@ -99,22 +94,12 @@ def encrypt( 'argon2id', '--cipher', cipher, + '--hash', + hash_type, + '--key-size', + str(resolved_key_size), ] - if is_aead: - # AEAD ciphers (e.g. chacha20-random) authenticate each sector via - # --integrity and do not take --hash or a fixed --key-size the same - # way length-preserving modes (aes-xts-plain64 etc.) do; key size is - # derived from the cipher/integrity combination by cryptsetup itself. - cmd += ['--integrity', cast(str, integrity)] - else: - cmd += [ - '--hash', - hash_type, - '--key-size', - str(key_size), - ] - cmd += [ '--iter-time', str(iter_time), diff --git a/archinstall/lib/models/device.py b/archinstall/lib/models/device.py index 50d8206810..b492762f1d 100644 --- a/archinstall/lib/models/device.py +++ b/archinstall/lib/models/device.py @@ -1464,15 +1464,15 @@ def type_to_text(self) -> str: DEFAULT_CIPHER = 'aes-xts-plain64' -# AEAD (Authenticated Encryption with Associated Data) ciphers require LUKS2 -# and a separate --integrity parameter; they cannot be used like normal -# block-cipher + chainmode + ivmode strings (e.g. aes-xts-plain64). -# Mapping: cipher value used in --cipher -> required --integrity value -AEAD_CIPHERS: dict[str, str] = { - 'aes-gcm-random': 'aead', +# XTS mode splits the key in two halves so needs double the bits. +# CBC/ECB use the key directly — max 256 bit. +CIPHER_KEY_SIZES: dict[str, int] = { + 'aes-xts-plain64': 512, # XTS: 256+256 + 'aes-cbc-essiv:sha256': 256, # CBC: single 256-bit key + 'serpent-xts-plain64': 512, # XTS: 256+256 + 'twofish-xts-plain64': 512, # XTS: 256+256 } - class _DiskEncryptionSerialization(TypedDict): encryption_type: str partitions: list[str] @@ -1480,7 +1480,6 @@ class _DiskEncryptionSerialization(TypedDict): hsm_device: NotRequired[_Fido2DeviceSerialization] iter_time: NotRequired[int] cipher: NotRequired[str] - integrity: NotRequired[str] @dataclass @@ -1492,7 +1491,6 @@ class DiskEncryption: hsm_device: Fido2Device | None = None iter_time: int = DEFAULT_ITER_TIME cipher: str = DEFAULT_CIPHER - integrity: str | None = None def __post_init__(self) -> None: if self.encryption_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS] and not self.partitions: @@ -1501,20 +1499,6 @@ def __post_init__(self) -> None: if self.encryption_type == EncryptionType.LUKS_ON_LVM and not self.lvm_volumes: raise ValueError('LuksOnLvm encryption require LMV volumes to be defined') - # AEAD ciphers (chacha20-random, aes-gcm-random) are only valid on LUKS2 - # and require --integrity to be set; auto-fill it if missing so the - # cryptsetup call downstream doesn't end up with an invalid combination. - if self.cipher in AEAD_CIPHERS: - required_integrity = AEAD_CIPHERS[self.cipher] - if not self.integrity: - self.integrity = required_integrity - elif self.integrity != required_integrity: - raise ValueError( - f'Cipher {self.cipher!r} requires integrity {required_integrity!r}, got {self.integrity!r}' - ) - elif self.integrity: - raise ValueError(f'Integrity option is only valid for AEAD ciphers, not {self.cipher!r}') - def should_generate_encryption_file(self, dev: PartitionModification | LvmVolume) -> bool: if isinstance(dev, PartitionModification): return dev in self.partitions and dev.mountpoint != Path('/') @@ -1537,9 +1521,6 @@ def json(self) -> _DiskEncryptionSerialization: if self.cipher != DEFAULT_CIPHER: # Only include if not default obj['cipher'] = self.cipher - if self.integrity: - obj['integrity'] = self.integrity - return obj @staticmethod @@ -1585,7 +1566,6 @@ def parse_arg( volumes.append(vol) cipher = disk_encryption.get('cipher', None) or DEFAULT_CIPHER - integrity = disk_encryption.get('integrity', None) enc = cls( EncryptionType(disk_encryption['encryption_type']), @@ -1593,7 +1573,6 @@ def parse_arg( enc_partitions, volumes, cipher=cipher, - integrity=integrity, ) if hsm := disk_encryption.get('hsm_device', None): From 1c471d201a45152d086c432bca729b3bb399942e Mon Sep 17 00:00:00 2001 From: Alperen42v <243298691+Alperen42v@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:37:04 +0300 Subject: [PATCH 4/8] ruff test fix --- archinstall/lib/disk/device_handler.py | 2 +- archinstall/lib/disk/encryption_menu.py | 2 +- archinstall/lib/disk/luks.py | 2 +- archinstall/lib/models/device.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index f2f7908896..9bb938219e 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -627,4 +627,4 @@ def wipe_dev(self, block_device: BDevice) -> None: self._wipe(block_device.device_info.path) -device_handler = DeviceHandler() \ No newline at end of file +device_handler = DeviceHandler() diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 30ae1a7ff8..248a055f16 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -458,4 +458,4 @@ async def select_encryption_cipher(preset: str | None = None) -> str | None: case ResultType.Skip: return preset case ResultType.Selection: - return result.get_value() \ No newline at end of file + return result.get_value() diff --git a/archinstall/lib/disk/luks.py b/archinstall/lib/disk/luks.py index ff2e529b47..ff0abcaca1 100644 --- a/archinstall/lib/disk/luks.py +++ b/archinstall/lib/disk/luks.py @@ -313,4 +313,4 @@ def unlock_luks2_dev( if not luks_handler.is_unlocked(): luks_handler.unlock() - return luks_handler \ No newline at end of file + return luks_handler diff --git a/archinstall/lib/models/device.py b/archinstall/lib/models/device.py index b492762f1d..b1ed353ea9 100644 --- a/archinstall/lib/models/device.py +++ b/archinstall/lib/models/device.py @@ -1664,4 +1664,4 @@ def serialize_size(self, size: Size) -> str: @classmethod def fields(cls) -> list[str]: - return [field.alias or name for name, field in cls.model_fields.items() if name != 'children'] \ No newline at end of file + return [field.alias or name for name, field in cls.model_fields.items() if name != 'children'] From 45cf08a3be1b28fefc891047c9720f2ee108ca7c Mon Sep 17 00:00:00 2001 From: Alperen42v <243298691+Alperen42v@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:17:33 +0300 Subject: [PATCH 5/8] luksfix xfs file system fix cipher --- archinstall/lib/disk/luks.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/archinstall/lib/disk/luks.py b/archinstall/lib/disk/luks.py index ff0abcaca1..275722cc4c 100644 --- a/archinstall/lib/disk/luks.py +++ b/archinstall/lib/disk/luks.py @@ -195,7 +195,14 @@ def lock(self) -> None: for child in lsblk_info.children: for mountpoint in child.mountpoints: debug(f'Unmounting {mountpoint}') - umount(mountpoint, recursive=True) + # mountpoint is a directory path, not a block device — umount() + # internally calls get_lsblk_info() which runs lsblk on the path + # and fails with "not a block device". Use run() directly to call + # umount(8) on the directory instead. + try: + run(['umount', '--recursive', str(mountpoint)]) + except Exception as e: + debug(f'Could not unmount {mountpoint}: {e}') # Wait for udev to finish processing events so the kernel drops # any lingering reference on the mapper device before we close it. @@ -313,4 +320,4 @@ def unlock_luks2_dev( if not luks_handler.is_unlocked(): luks_handler.unlock() - return luks_handler + return luks_handler \ No newline at end of file From f8a202d843636e26f1c19fbdae87a96479127147 Mon Sep 17 00:00:00 2001 From: Alperen42v <243298691+Alperen42v@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:55:24 +0300 Subject: [PATCH 6/8] ruff fix --- archinstall/lib/disk/luks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archinstall/lib/disk/luks.py b/archinstall/lib/disk/luks.py index 275722cc4c..d3d7e707c1 100644 --- a/archinstall/lib/disk/luks.py +++ b/archinstall/lib/disk/luks.py @@ -320,4 +320,4 @@ def unlock_luks2_dev( if not luks_handler.is_unlocked(): luks_handler.unlock() - return luks_handler \ No newline at end of file + return luks_handler From ed7ba45ae3a8660abc62be651defe2d124f7dc11 Mon Sep 17 00:00:00 2001 From: Alperen42v <243298691+Alperen42v@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:12:59 +0300 Subject: [PATCH 7/8] new ciphers --- archinstall/lib/disk/device_handler.py | 5 +-- archinstall/lib/disk/encryption_menu.py | 27 ++++++-------- archinstall/lib/disk/luks.py | 16 +++------ archinstall/lib/models/device.py | 48 ++++++++++++++++++------- 4 files changed, 54 insertions(+), 42 deletions(-) diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 9bb938219e..f208a957b5 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -23,6 +23,7 @@ BtrfsMountOption, DeviceModification, DiskEncryption, + EncryptionCipher, FilesystemType, LsblkInfo, ModificationStatus, @@ -281,7 +282,7 @@ def encrypt( enc_password: Password | None, lock_after_create: bool = True, iter_time: int = DEFAULT_ITER_TIME, - cipher: str = DEFAULT_CIPHER, + cipher: EncryptionCipher = DEFAULT_CIPHER, ) -> Luks2: luks_handler = Luks2( dev_path, @@ -627,4 +628,4 @@ def wipe_dev(self, block_device: BDevice) -> None: self._wipe(block_device.device_info.path) -device_handler = DeviceHandler() +device_handler = DeviceHandler() \ No newline at end of file diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 248a055f16..124eaccc17 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -11,6 +11,7 @@ DEFAULT_ITER_TIME, DeviceModification, DiskEncryption, + EncryptionCipher, EncryptionType, Fido2Device, LvmConfiguration, @@ -112,7 +113,7 @@ async def _select_lvm_vols(self, preset: list[LvmVolume]) -> list[LvmVolume]: return await select_lvm_vols_to_encrypt(self._lvm_config, preset=preset) return [] - async def _select_cipher(self, preset: str | None) -> str | None: + async def _select_cipher(self, preset: EncryptionCipher | None) -> EncryptionCipher | None: return await select_encryption_cipher(preset) def _check_dep_enc_type(self) -> bool: @@ -141,7 +142,7 @@ async def show(self) -> DiskEncryption | None: enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value enc_password: Password | None = self._item_group.find_by_key('encryption_password').value - cipher: str | None = self._item_group.find_by_key('cipher').value + cipher: EncryptionCipher | None = self._item_group.find_by_key('cipher').value iter_time: int | None = self._item_group.find_by_key('iter_time').value enc_partitions = self._item_group.find_by_key('partitions').value enc_lvm_vols = self._item_group.find_by_key('lvm_volumes').value @@ -214,9 +215,9 @@ def _prev_password(self, item: MenuItem) -> str | None: return None def _prev_cipher(self, item: MenuItem) -> str | None: - cipher = self._item_group.find_by_key('cipher').value + cipher: EncryptionCipher | None = self._item_group.find_by_key('cipher').value if cipher: - return f'{tr("Encryption cipher")}: {cipher}' + return f'{tr("Encryption cipher")}: {cipher.value}' return None def _prev_partitions(self, item: MenuItem) -> str | None: @@ -429,23 +430,17 @@ def validate_iter_time(value: str) -> str | None: return None -async def select_encryption_cipher(preset: str | None = None) -> str | None: - # Regular block-cipher modes: accepted directly by `cryptsetup --cipher`. - options = [ - 'aes-xts-plain64', - 'aes-cbc-essiv:sha256', - 'serpent-xts-plain64', - 'twofish-xts-plain64', - ] +async def select_encryption_cipher(preset: EncryptionCipher | None = None) -> EncryptionCipher | None: + options = list(EncryptionCipher) if not preset: - preset = options[0] + preset = DEFAULT_CIPHER - items = [MenuItem(o, value=o) for o in options] + items = [MenuItem(o.value, value=o) for o in options] group = MenuItemGroup(items) group.set_focus_by_value(preset) - result = await Selection[str]( + result = await Selection[EncryptionCipher]( group, header=tr('Select encryption cipher'), allow_skip=True, @@ -458,4 +453,4 @@ async def select_encryption_cipher(preset: str | None = None) -> str | None: case ResultType.Skip: return preset case ResultType.Selection: - return result.get_value() + return result.get_value() \ No newline at end of file diff --git a/archinstall/lib/disk/luks.py b/archinstall/lib/disk/luks.py index d3d7e707c1..1632571202 100644 --- a/archinstall/lib/disk/luks.py +++ b/archinstall/lib/disk/luks.py @@ -8,7 +8,7 @@ from archinstall.lib.disk.utils import get_lsblk_info, umount from archinstall.lib.exceptions import DiskError, SysCallError from archinstall.lib.log import debug, info -from archinstall.lib.models.device import CIPHER_KEY_SIZES, DEFAULT_CIPHER, DEFAULT_ITER_TIME +from archinstall.lib.models.device import DEFAULT_CIPHER, DEFAULT_ITER_TIME, EncryptionCipher from archinstall.lib.models.users import Password from archinstall.lib.utils.util import generate_password @@ -69,21 +69,15 @@ def _get_passphrase_args( def encrypt( self, - key_size: int = 512, hash_type: str = 'sha512', iter_time: int = DEFAULT_ITER_TIME, key_file: Path | None = None, - cipher: str = DEFAULT_CIPHER, + cipher: EncryptionCipher = DEFAULT_CIPHER, ) -> Path | None: debug(f'Luks2 encrypting: {self.luks_dev_path}') key_file_arg, passphrase = self._get_passphrase_args(key_file) - # Resolve the correct key size for this cipher. - # XTS mode uses two keys (e.g. 256+256 = 512 bits for aes-xts-plain64), - # CBC mode uses a single key (max 256 bits for aes-cbc-essiv:sha256). - resolved_key_size = CIPHER_KEY_SIZES.get(cipher, key_size) - cmd = [ 'cryptsetup', '--batch-mode', @@ -93,11 +87,11 @@ def encrypt( '--pbkdf', 'argon2id', '--cipher', - cipher, + cipher.value, '--hash', hash_type, '--key-size', - str(resolved_key_size), + str(cipher.key_size), ] cmd += [ @@ -320,4 +314,4 @@ def unlock_luks2_dev( if not luks_handler.is_unlocked(): luks_handler.unlock() - return luks_handler + return luks_handler \ No newline at end of file diff --git a/archinstall/lib/models/device.py b/archinstall/lib/models/device.py index b1ed353ea9..74d7fb6e91 100644 --- a/archinstall/lib/models/device.py +++ b/archinstall/lib/models/device.py @@ -1462,16 +1462,37 @@ def type_to_text(self) -> str: return type_to_text[self] -DEFAULT_CIPHER = 'aes-xts-plain64' +class EncryptionCipher(Enum): + # None passed to cryptsetup means its built-in default (aes-xts-plain64). + # Adiantum is for CPUs without AES acceleration. It is a composite mode: + # spec must name both the stream cipher and block cipher + # (xchacha12,aes) or the kernel rejects it as unsupported. + AES_XTS_PLAIN64 = 'aes-xts-plain64' + # xchacha12 = faster (Android default), xchacha20 = wider margin. + ADIANTUM_XCHACHA12_PLAIN64 = 'xchacha12,aes-adiantum-plain64' + ADIANTUM_XCHACHA20_PLAIN64 = 'xchacha20,aes-adiantum-plain64' + # AES finalist, conservative margin (32 rounds), bitslices well + # on AVX2 despite no dedicated hw acceleration. + SERPENT_XTS_PLAIN64 = 'serpent-xts-plain64' + # Wide-block AES mode (AES-NI accelerated), single 256-bit key. + AES_HCTR2_PLAIN64 = 'aes-hctr2-plain64' + # Non-NIST standard (ISO/NESSIE/CRYPTREC), AVX2 accelerated. + CAMELLIA_XTS_PLAIN64 = 'camellia-xts-plain64' + # Legacy CBC mode — weaker than XTS against watermarking attacks, + # slower due to per-sector ESSIV/SHA256. Included for compatibility. + AES_CBC_ESSIV_SHA256 = 'aes-cbc-essiv:sha256' + + @property + def key_size(self) -> int: + # XTS uses two keys, so 512 bits => 256-bit cipher. Adiantum + # and HCTR2 use a single 256-bit key; 512 makes cryptsetup fail. + if '-xts-' in self.value: + return 512 + return 256 + + +DEFAULT_CIPHER = EncryptionCipher.AES_XTS_PLAIN64 -# XTS mode splits the key in two halves so needs double the bits. -# CBC/ECB use the key directly — max 256 bit. -CIPHER_KEY_SIZES: dict[str, int] = { - 'aes-xts-plain64': 512, # XTS: 256+256 - 'aes-cbc-essiv:sha256': 256, # CBC: single 256-bit key - 'serpent-xts-plain64': 512, # XTS: 256+256 - 'twofish-xts-plain64': 512, # XTS: 256+256 -} class _DiskEncryptionSerialization(TypedDict): encryption_type: str @@ -1490,7 +1511,7 @@ class DiskEncryption: lvm_volumes: list[LvmVolume] = field(default_factory=list) hsm_device: Fido2Device | None = None iter_time: int = DEFAULT_ITER_TIME - cipher: str = DEFAULT_CIPHER + cipher: EncryptionCipher = DEFAULT_CIPHER def __post_init__(self) -> None: if self.encryption_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS] and not self.partitions: @@ -1519,7 +1540,7 @@ def json(self) -> _DiskEncryptionSerialization: obj['iter_time'] = self.iter_time if self.cipher != DEFAULT_CIPHER: # Only include if not default - obj['cipher'] = self.cipher + obj['cipher'] = self.cipher.value return obj @@ -1565,7 +1586,8 @@ def parse_arg( if vol.obj_id in disk_encryption.get('lvm_volumes', []): volumes.append(vol) - cipher = disk_encryption.get('cipher', None) or DEFAULT_CIPHER + cipher_str = disk_encryption.get('cipher', None) + cipher = EncryptionCipher(cipher_str) if cipher_str else DEFAULT_CIPHER enc = cls( EncryptionType(disk_encryption['encryption_type']), @@ -1664,4 +1686,4 @@ def serialize_size(self, size: Size) -> str: @classmethod def fields(cls) -> list[str]: - return [field.alias or name for name, field in cls.model_fields.items() if name != 'children'] + return [field.alias or name for name, field in cls.model_fields.items() if name != 'children'] \ No newline at end of file From 7fcdec0d32ef7b6099f8426bda1bbd529269d47c Mon Sep 17 00:00:00 2001 From: Alperen42v <243298691+Alperen42v@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:34:40 +0300 Subject: [PATCH 8/8] yes yes ruff fix --- archinstall/lib/disk/device_handler.py | 2 +- archinstall/lib/disk/encryption_menu.py | 2 +- archinstall/lib/disk/luks.py | 2 +- archinstall/lib/models/device.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index f208a957b5..e00e11b12c 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -628,4 +628,4 @@ def wipe_dev(self, block_device: BDevice) -> None: self._wipe(block_device.device_info.path) -device_handler = DeviceHandler() \ No newline at end of file +device_handler = DeviceHandler() diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 124eaccc17..1a1b34bb90 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -453,4 +453,4 @@ async def select_encryption_cipher(preset: EncryptionCipher | None = None) -> En case ResultType.Skip: return preset case ResultType.Selection: - return result.get_value() \ No newline at end of file + return result.get_value() diff --git a/archinstall/lib/disk/luks.py b/archinstall/lib/disk/luks.py index 1632571202..c5f907a963 100644 --- a/archinstall/lib/disk/luks.py +++ b/archinstall/lib/disk/luks.py @@ -314,4 +314,4 @@ def unlock_luks2_dev( if not luks_handler.is_unlocked(): luks_handler.unlock() - return luks_handler \ No newline at end of file + return luks_handler diff --git a/archinstall/lib/models/device.py b/archinstall/lib/models/device.py index 74d7fb6e91..e2f002b7c0 100644 --- a/archinstall/lib/models/device.py +++ b/archinstall/lib/models/device.py @@ -1686,4 +1686,4 @@ def serialize_size(self, size: Size) -> str: @classmethod def fields(cls) -> list[str]: - return [field.alias or name for name, field in cls.model_fields.items() if name != 'children'] \ No newline at end of file + return [field.alias or name for name, field in cls.model_fields.items() if name != 'children']