feat(disk): add custom cipher selection for LUKS2 encryption (#4581)

* feat(disk): fix menu rendering and complete backend data pipeline for custom LUKS2 cipher

* fix : removed chacha20

* fix the problems for filesystems

* ruff test fix

* luksfix xfs file system fix cipher

* ruff fix

* new ciphers

* yes yes ruff fix

* Device.py : removed bloat commets

* refactor(disk): address review feedback on cipher selection

- always serialize iter_time and cipher in DiskEncryption.json
- simplify select_encryption_cipher using MenuItemGroup.from_enum
- add cipher to _prev_disk_encryption preview in disk_menu.py
- drop unnecessary inline comments from EncryptionCipher enum
This commit is contained in:
Alperen42v 2026-08-11 13:18:42 +03:00 committed by GitHub
parent f4e0d4cf66
commit 7b7fd6573e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 136 additions and 10 deletions

View File

@ -20,11 +20,13 @@ from archinstall.lib.exceptions import DiskError, SysCallError, UnknownFilesyste
from archinstall.lib.hardware import SysInfo
from archinstall.lib.log import debug, error, info, log
from archinstall.lib.models.device import (
DEFAULT_CIPHER,
DEFAULT_ITER_TIME,
BDevice,
BtrfsMountOption,
DeviceModification,
DiskEncryption,
EncryptionCipher,
FilesystemType,
LsblkInfo,
ModificationStatus,
@ -282,6 +284,7 @@ class DeviceHandler:
enc_password: Password | None,
lock_after_create: bool = True,
iter_time: int = DEFAULT_ITER_TIME,
cipher: EncryptionCipher = DEFAULT_CIPHER,
) -> Luks2:
luks_handler = Luks2(
dev_path,
@ -289,7 +292,7 @@ class DeviceHandler:
password=enc_password,
)
key_file = luks_handler.encrypt(iter_time=iter_time)
key_file = luks_handler.encrypt(iter_time=iter_time, cipher=cipher)
udev_sync()
@ -320,7 +323,7 @@ class DeviceHandler:
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)
udev_sync()
@ -332,6 +335,8 @@ class DeviceHandler:
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()

View File

@ -283,6 +283,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
if enc_type != EncryptionType.NO_ENCRYPTION:
output += tr('Iteration time') + f': {enc_config.iter_time or DEFAULT_ITER_TIME}ms\n'
output += tr('Cipher') + f': {enc_config.cipher.value}\n'
if enc_config.partitions:
output += f'Partitions: {len(enc_config.partitions)} selected\n'

View File

@ -7,9 +7,11 @@ from archinstall.lib.menu.helpers import Input, Selection, Table
from archinstall.lib.menu.menu_helper import MenuHelper
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.device import (
DEFAULT_CIPHER,
DEFAULT_ITER_TIME,
DeviceModification,
DiskEncryption,
EncryptionCipher,
EncryptionType,
Fido2Device,
LvmConfiguration,
@ -64,6 +66,14 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
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,9 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
return await select_lvm_vols_to_encrypt(self._lvm_config, preset=preset)
return []
async def _select_cipher(self, preset: EncryptionCipher | None) -> EncryptionCipher | None:
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
if enc_type and enc_type != EncryptionType.NO_ENCRYPTION:
@ -129,6 +142,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
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: 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
@ -151,6 +165,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
lvm_volumes=enc_lvm_vols,
hsm_device=enc_config.hsm_device,
iter_time=iter_time or DEFAULT_ITER_TIME,
cipher=cipher or DEFAULT_CIPHER,
)
return None
@ -164,6 +179,9 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
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 +214,12 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
return None
def _prev_cipher(self, item: MenuItem) -> str | None:
cipher: EncryptionCipher | None = self._item_group.find_by_key('cipher').value
if cipher:
return f'{tr("Encryption cipher")}: {cipher.value}'
return None
def _prev_partitions(self, item: MenuItem) -> str | None:
if item.value:
output = tr('Partitions to be encrypted') + '\n'
@ -404,3 +428,23 @@ async def select_iteration_time(preset: int | None = None) -> int | None:
return int(result.get_value())
case ResultType.Reset:
return None
async def select_encryption_cipher(preset: EncryptionCipher | None = None) -> EncryptionCipher | None:
group = MenuItemGroup.from_enum(EncryptionCipher)
group.set_focus_by_value(preset or DEFAULT_CIPHER)
result = await Selection[EncryptionCipher](
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()

View File

@ -8,7 +8,7 @@ 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 DEFAULT_CIPHER, DEFAULT_ITER_TIME, EncryptionCipher
from archinstall.lib.models.users import Password
from archinstall.lib.utils.util import generate_password
@ -69,10 +69,10 @@ class Luks2:
def encrypt(
self,
key_size: int = 512,
hash_type: str = 'sha512',
iter_time: int = DEFAULT_ITER_TIME,
key_file: Path | None = None,
cipher: EncryptionCipher = DEFAULT_CIPHER,
) -> Path | None:
debug(f'Luks2 encrypting: {self.luks_dev_path}')
@ -86,10 +86,15 @@ class Luks2:
'luks2',
'--pbkdf',
'argon2id',
'--cipher',
cipher.value,
'--hash',
hash_type,
'--key-size',
str(key_size),
str(cipher.key_size),
]
cmd += [
'--iter-time',
str(iter_time),
*key_file_arg,
@ -137,6 +142,16 @@ class Luks2:
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 = [
@ -161,6 +176,8 @@ class Luks2:
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
@ -169,14 +186,48 @@ class Luks2:
# 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)
# 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.
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:
"""

View File

@ -1459,12 +1459,32 @@ class EncryptionType(StrEnum):
return type_to_text[self]
class EncryptionCipher(Enum):
AES_XTS_PLAIN64 = 'aes-xts-plain64'
ADIANTUM_XCHACHA12_PLAIN64 = 'xchacha12,aes-adiantum-plain64'
ADIANTUM_XCHACHA20_PLAIN64 = 'xchacha20,aes-adiantum-plain64'
SERPENT_XTS_PLAIN64 = 'serpent-xts-plain64'
AES_HCTR2_PLAIN64 = 'aes-hctr2-plain64'
CAMELLIA_XTS_PLAIN64 = 'camellia-xts-plain64'
AES_CBC_ESSIV_SHA256 = 'aes-cbc-essiv:sha256'
@property
def key_size(self) -> int:
if '-xts-' in self.value:
return 512
return 256
DEFAULT_CIPHER = EncryptionCipher.AES_XTS_PLAIN64
class _DiskEncryptionSerialization(TypedDict):
encryption_type: str
partitions: list[str]
lvm_volumes: list[str]
hsm_device: NotRequired[_Fido2DeviceSerialization]
iter_time: NotRequired[int]
cipher: NotRequired[str]
@dataclass
@ -1475,6 +1495,7 @@ class DiskEncryption:
lvm_volumes: list[LvmVolume] = field(default_factory=list)
hsm_device: Fido2Device | None = None
iter_time: int = DEFAULT_ITER_TIME
cipher: EncryptionCipher = DEFAULT_CIPHER
def __post_init__(self) -> None:
if self.encryption_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS] and not self.partitions:
@ -1499,8 +1520,8 @@ class DiskEncryption:
if self.hsm_device:
obj['hsm_device'] = self.hsm_device.json()
if self.iter_time != DEFAULT_ITER_TIME: # Only include if not default
obj['iter_time'] = self.iter_time
obj['iter_time'] = self.iter_time
obj['cipher'] = self.cipher.value
return obj
@ -1546,11 +1567,15 @@ class DiskEncryption:
if vol.obj_id in disk_encryption.get('lvm_volumes', []):
volumes.append(vol)
cipher_str = disk_encryption.get('cipher', None)
cipher = EncryptionCipher(cipher_str) if cipher_str else DEFAULT_CIPHER
enc = cls(
EncryptionType(disk_encryption['encryption_type']),
password,
enc_partitions,
volumes,
cipher=cipher,
)
if hsm := disk_encryption.get('hsm_device', None):