From 4483b0fb0181c391619f4edea571ecf0e79b2d50 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Fri, 6 Sep 2024 12:20:20 +0000 Subject: [PATCH] mmgen-autosign: disable swap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent the volatile memory to which temporary offline signing wallets are written from being swapped to disk. This feature is supported on both Linux and macOS. Swap is disabled automatically during the setup process. It can also be disabled manually using the ‘disable_swap’ command. Compressed RAM (zram) swap is left untouched. Regarding vulnerability of existing setups, please note the following mitigating factors: a) mmgen-autosign encrypts all temporary signing wallets with random 16- or 32-byte passwords; b) some platforms swap to compressed RAM instead of disk (SoCs running Armbian, for example); c) swap partitions are often encrypted; and d) even on platforms that do swap to unencrypted files or partitions, the likelihood of the relevant pages of RAM being swapped out on a machine used exclusively for offline signing is minimal. For peace of mind, existing users may wish to wipe the autosign key on their removable device. Those concerned about metadata leakage might additionally zero the swap partition(s) on their offline signing machine(s). --- mmgen/autosign.py | 102 ++++++++++++++++++++++++++++++++++++++++- mmgen/data/version | 2 +- mmgen/main_autosign.py | 9 ++++ pyproject.toml | 1 + 4 files changed, 111 insertions(+), 3 deletions(-) diff --git a/mmgen/autosign.py b/mmgen/autosign.py index e1e1ad24..b902c4bf 100755 --- a/mmgen/autosign.py +++ b/mmgen/autosign.py @@ -18,13 +18,105 @@ from pathlib import Path from subprocess import run, PIPE, DEVNULL from .cfg import Config -from .util import msg, msg_r, ymsg, rmsg, gmsg, bmsg, die, suf, fmt, fmt_list, is_int -from .color import yellow,red,orange,brown +from .util import msg, msg_r, ymsg, rmsg, gmsg, bmsg, die, suf, fmt, fmt_list, is_int, have_sudo, capfirst +from .color import yellow, red, orange, brown, blue from .wallet import Wallet,get_wallet_cls from .addrlist import AddrIdxList from .filename import find_file_in_dir from .ui import keypress_confirm +def SwapMgr(*args, **kwargs): + if sys.platform == 'linux': + return SwapMgrLinux(*args, **kwargs) + elif sys.platform == 'darwin': + return SwapMgrMacOS(*args, **kwargs) + +class SwapMgrBase: + + def __init__(self, cfg, ignore_zram=False): + self.cfg = cfg + self.ignore_zram = ignore_zram + self.desc = 'disk swap' if ignore_zram else 'swap' + + def enable(self, quiet=False): + ret = self.do_enable() + if not quiet: + self.cfg._util.qmsg( + f'{capfirst(self.desc)} successfully enabled' if ret else + f'{capfirst(self.desc)} is already enabled' if ret is None else + f'Could not enable {self.desc}') + return ret + + def disable(self, quiet=False): + self.cfg._util.qmsg_r(f'Disabling {self.desc}...') + ret = self.do_disable() + self.cfg._util.qmsg('done') + if not quiet: + self.cfg._util.qmsg( + f'{capfirst(self.desc)} successfully disabled ({fmt_list(ret, fmt="no_quotes")})' + if ret and isinstance(ret, list) else + f'{capfirst(self.desc)} successfully disabled' if ret else + f'No active {self.desc}') + return ret + + def process_cmds(self, op, cmds): + if not cmds: + return + if have_sudo(silent=True) and not self.cfg.test_suite: + for cmd in cmds: + run(cmd.split(), check=True) + else: + nl = '\n' if op == 'disable' else '' + fs = blue('{a} {b} by executing the following command{c}:\n{d}') + m = nl + fs.format( + a = 'Before continuing, please disable' if op == 'disable' else 'Enable', + b = self.desc, + c = suf(cmds), + d = fmt_list(cmds, indent=' ', fmt='col')) + msg(m) + if not self.cfg.test_suite: + sys.exit(1) + +class SwapMgrLinux(SwapMgrBase): + + def get_active(self): + cp = run(['/sbin/swapon', '--show=NAME', '--noheadings'], stdout=PIPE, text=True, check=True) + res = cp.stdout.splitlines() + return [e for e in res if not e.startswith('/dev/zram')] if self.ignore_zram else res + + def do_enable(self): + if ret := self.get_active(): + ymsg(f'Warning: {self.desc} is already enabled: ({fmt_list(ret, fmt="no_quotes")})') + self.process_cmds('enable', ['sudo swapon --all']) + return True + + def do_disable(self): + swapdevs = self.get_active() + if not swapdevs: + return None + self.process_cmds('disable', [f'sudo swapoff {swapdev}' for swapdev in swapdevs]) + return swapdevs + +class SwapMgrMacOS(SwapMgrBase): + + def get_active(self): + cmd = 'launchctl print system/com.apple.dynamic_pager' + return run(cmd.split(), stdout=DEVNULL, stderr=DEVNULL).returncode == 0 + + def _do_action(self, active, op, cmd): + if self.get_active() is active: + return None + else: + cmd = f'sudo launchctl {cmd} -w /System/Library/LaunchDaemons/com.apple.dynamic_pager.plist' + self.process_cmds(op, [cmd]) + return True + + def do_enable(self): + return self._do_action(active=True, op='enable', cmd='load') + + def do_disable(self): + return self._do_action(active=False, op='disable', cmd='unload') + class Signable: non_xmr_signables = ( @@ -336,6 +428,8 @@ class Autosign: 'gen_key', 'macos_ramdisk_setup', 'macos_ramdisk_delete', + 'enable_swap', + 'disable_swap', 'clean', 'wipe_key') @@ -450,6 +544,8 @@ class Autosign: for name,path in self.dirs.items(): setattr(self, name, self.mountpoint / path) + self.swap = SwapMgr(self.cfg, ignore_zram=True) + async def check_daemons_running(self): from .protocol import init_proto for coin in self.coins: @@ -666,6 +762,8 @@ class Autosign: self.gen_key(no_unmount=True) + self.swap.disable() + if sys.platform == 'darwin': self.macos_ramdisk_setup() diff --git a/mmgen/data/version b/mmgen/data/version index d8529835..6394b053 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -15.0.dev6 +15.0.dev7 diff --git a/mmgen/main_autosign.py b/mmgen/main_autosign.py index 53cc9cc3..ffd54e6b 100755 --- a/mmgen/main_autosign.py +++ b/mmgen/main_autosign.py @@ -76,6 +76,11 @@ macos_ramdisk_setup - set up the ramdisk used for storing the temporary signing wallet(s) (macOS only). Required only when creating the wallet(s) manually, without ‘setup’ macos_ramdisk_delete - delete the macOS ramdisk +disable_swap - disable disk swap to prevent potentially sensitive data in + volatile memory from being swapped to disk. Applicable only when + creating temporary signing wallet(s) manually, without ‘setup’ +enable_swap - reenable disk swap. For testing only, should not be invoked in + a production environment wait - start in loop mode: wait-mount-sign-unmount-wait wipe_key - wipe the wallet encryption key on the removable device, making signing transactions or stealing the user’s seed impossible. @@ -227,6 +232,10 @@ elif cmd.startswith('macos_ramdisk'): if sys.platform != 'darwin': die(1, f'The ‘{cmd}’ operation is for the macOS platform only') getattr(asi, cmd)() +elif cmd == 'enable_swap': + asi.swap.enable() +elif cmd == 'disable_swap': + asi.swap.disable() elif cmd == 'sign': main(do_loop=False) elif cmd == 'wait': diff --git a/pyproject.toml b/pyproject.toml index ec03e0d3..cfd9f325 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,4 +81,5 @@ ignored-classes = [ # ignored for no-member, otherwise checked "TestHashFunc", "GenTool", "VirtBlockDeviceBase", + "SwapMgrBase", ]