From 3c8c9ce42e21a2be17c253237bf06d5f98266555 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Feb 2026 20:58:35 +0000 Subject: [PATCH 1/4] Move auth file to $XDG_RUNTIME_DIR for improved security Use $XDG_RUNTIME_DIR/keepmenu/ for storing the auth file instead of ~/.cache/. This provides better security because: - tmpfs-backed: auth keys never written to persistent disk storage - Auto-cleanup: files removed automatically on user logout - Enforced permissions: systemd guarantees 0700 on the directory Falls back to ~/.cache/ if XDG_RUNTIME_DIR is not available. https://claude.ai/code/session_013sXF2YWWf8yW5hRuhGVbZU --- keepmenu/__init__.py | 26 ++++++++++++++++++++++++-- keepmenu/__main__.py | 5 ++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/keepmenu/__init__.py b/keepmenu/__init__.py index 554d636..ecca2e2 100644 --- a/keepmenu/__init__.py +++ b/keepmenu/__init__.py @@ -8,7 +8,7 @@ import shlex from subprocess import run, DEVNULL import sys -from os.path import exists, expanduser +from os.path import exists, expanduser, join from keepmenu.menu import dmenu_err @@ -22,7 +22,29 @@ # file_handler.setFormatter(formatter) # logger.addHandler(file_handler) -AUTH_FILE = expanduser("~/.cache/.keepmenu-auth") + +def get_runtime_dir(): + """Get the runtime directory for auth file storage. + + Prefers $XDG_RUNTIME_DIR/keepmenu/ for security (tmpfs-backed, auto-cleanup + on logout, proper permissions enforced by systemd). Falls back to ~/.cache/ + if XDG_RUNTIME_DIR is not available. + + Returns: str path to runtime directory + + """ + xdg_runtime = os.environ.get('XDG_RUNTIME_DIR') + if xdg_runtime and exists(xdg_runtime): + runtime_dir = join(xdg_runtime, 'keepmenu') + else: + runtime_dir = expanduser("~/.cache") + # Ensure directory exists with secure permissions + if not exists(runtime_dir): + os.makedirs(runtime_dir, mode=0o700) + return runtime_dir + + +AUTH_FILE = join(get_runtime_dir(), ".keepmenu-auth") CONF_FILE = expanduser("~/.config/keepmenu/config.ini") SECRET_VALID_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" diff --git a/keepmenu/__main__.py b/keepmenu/__main__.py index 8887755..647859a 100644 --- a/keepmenu/__main__.py +++ b/keepmenu/__main__.py @@ -41,7 +41,10 @@ def port_in_use(port): return s.connect_ex(('127.0.0.1', port)) == 0 def get_auth(): - """Generate and save port and authkey to ~/.cache/.keepmenu-auth + """Generate and save port and authkey to runtime directory. + + Uses $XDG_RUNTIME_DIR/keepmenu/ if available (tmpfs-backed, auto-cleanup), + otherwise falls back to ~/.cache/. Returns: int port, bytestring authkey From 9f84748f2199b3e2d0ad58b059a50cde8b077575 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Feb 2026 21:46:31 +0000 Subject: [PATCH 2/4] Use $TMPDIR as fallback instead of ~/.cache for auth file Change fallback from ~/.cache/ to $TMPDIR/keepmenu-/ when $XDG_RUNTIME_DIR is not available. This is more secure because: - $TMPDIR is typically tmpfs (memory-backed, not persisted to disk) - Cleared on reboot (no stale auth files) - Forensic recovery not possible - User-specific subdirectory with 0700 permissions ensures privacy https://claude.ai/code/session_013sXF2YWWf8yW5hRuhGVbZU --- keepmenu/__init__.py | 7 ++++--- keepmenu/__main__.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/keepmenu/__init__.py b/keepmenu/__init__.py index ecca2e2..dc64463 100644 --- a/keepmenu/__init__.py +++ b/keepmenu/__init__.py @@ -8,6 +8,7 @@ import shlex from subprocess import run, DEVNULL import sys +import tempfile from os.path import exists, expanduser, join from keepmenu.menu import dmenu_err @@ -27,8 +28,8 @@ def get_runtime_dir(): """Get the runtime directory for auth file storage. Prefers $XDG_RUNTIME_DIR/keepmenu/ for security (tmpfs-backed, auto-cleanup - on logout, proper permissions enforced by systemd). Falls back to ~/.cache/ - if XDG_RUNTIME_DIR is not available. + on logout, proper permissions enforced by systemd). Falls back to + $TMPDIR/keepmenu-/ which is also typically tmpfs and cleared on reboot. Returns: str path to runtime directory @@ -37,7 +38,7 @@ def get_runtime_dir(): if xdg_runtime and exists(xdg_runtime): runtime_dir = join(xdg_runtime, 'keepmenu') else: - runtime_dir = expanduser("~/.cache") + runtime_dir = join(tempfile.gettempdir(), f'keepmenu-{os.getuid()}') # Ensure directory exists with secure permissions if not exists(runtime_dir): os.makedirs(runtime_dir, mode=0o700) diff --git a/keepmenu/__main__.py b/keepmenu/__main__.py index 647859a..9593b20 100644 --- a/keepmenu/__main__.py +++ b/keepmenu/__main__.py @@ -44,7 +44,7 @@ def get_auth(): """Generate and save port and authkey to runtime directory. Uses $XDG_RUNTIME_DIR/keepmenu/ if available (tmpfs-backed, auto-cleanup), - otherwise falls back to ~/.cache/. + otherwise falls back to $TMPDIR/keepmenu-/. Returns: int port, bytestring authkey From 55f328fded7b256be759e39089d6e7bbebb5699d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Feb 2026 21:50:03 +0000 Subject: [PATCH 3/4] Add tests for get_runtime_dir() function Tests cover: - $XDG_RUNTIME_DIR used when set and exists - Fallback to $TMPDIR/keepmenu-/ when XDG_RUNTIME_DIR not set - Fallback when XDG_RUNTIME_DIR is set but path doesn't exist - Directory created with 0700 permissions https://claude.ai/code/session_013sXF2YWWf8yW5hRuhGVbZU --- tests/tests.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/tests.py b/tests/tests.py index eedb8a0..aec9a1e 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -19,6 +19,71 @@ SECRET2 = 'PW4YAYYZVDE5RK2AOLKUATNZIKAFQLZO' +class TestRuntimeDir(unittest.TestCase): + """Test get_runtime_dir() function for auth file location + + """ + def setUp(self): + self.tmpdir = tempfile.mkdtemp() + # Save original environment + self.orig_xdg_runtime = os.environ.get('XDG_RUNTIME_DIR') + self.orig_tmpdir = os.environ.get('TMPDIR') + + def tearDown(self): + rmtree(self.tmpdir) + # Restore original environment + if self.orig_xdg_runtime is not None: + os.environ['XDG_RUNTIME_DIR'] = self.orig_xdg_runtime + elif 'XDG_RUNTIME_DIR' in os.environ: + del os.environ['XDG_RUNTIME_DIR'] + if self.orig_tmpdir is not None: + os.environ['TMPDIR'] = self.orig_tmpdir + elif 'TMPDIR' in os.environ: + del os.environ['TMPDIR'] + + def test_xdg_runtime_dir_used_when_set(self): + """Test that $XDG_RUNTIME_DIR/keepmenu/ is used when available + """ + xdg_runtime = os.path.join(self.tmpdir, 'runtime') + os.makedirs(xdg_runtime, mode=0o700) + os.environ['XDG_RUNTIME_DIR'] = xdg_runtime + + runtime_dir = KM.get_runtime_dir() + + self.assertEqual(runtime_dir, os.path.join(xdg_runtime, 'keepmenu')) + self.assertTrue(os.path.exists(runtime_dir)) + self.assertEqual(os.stat(runtime_dir).st_mode & 0o777, 0o700) + + def test_tmpdir_fallback_when_xdg_runtime_unset(self): + """Test fallback to $TMPDIR/keepmenu-/ when XDG_RUNTIME_DIR not set + """ + if 'XDG_RUNTIME_DIR' in os.environ: + del os.environ['XDG_RUNTIME_DIR'] + custom_tmpdir = os.path.join(self.tmpdir, 'tmp') + os.makedirs(custom_tmpdir, mode=0o777) + os.environ['TMPDIR'] = custom_tmpdir + + runtime_dir = KM.get_runtime_dir() + + expected = os.path.join(custom_tmpdir, f'keepmenu-{os.getuid()}') + self.assertEqual(runtime_dir, expected) + self.assertTrue(os.path.exists(runtime_dir)) + self.assertEqual(os.stat(runtime_dir).st_mode & 0o777, 0o700) + + def test_tmpdir_fallback_when_xdg_runtime_dir_not_exists(self): + """Test fallback when XDG_RUNTIME_DIR is set but doesn't exist + """ + os.environ['XDG_RUNTIME_DIR'] = '/nonexistent/path' + custom_tmpdir = os.path.join(self.tmpdir, 'tmp') + os.makedirs(custom_tmpdir, mode=0o777) + os.environ['TMPDIR'] = custom_tmpdir + + runtime_dir = KM.get_runtime_dir() + + expected = os.path.join(custom_tmpdir, f'keepmenu-{os.getuid()}') + self.assertEqual(runtime_dir, expected) + + class TestServer(unittest.TestCase): """Test various BaseManager server functions From 81880600c42c2f41a79cf8f1c66627ceb32eefc0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Feb 2026 21:51:40 +0000 Subject: [PATCH 4/4] Fix TestRuntimeDir tests by resetting tempfile cache tempfile.gettempdir() caches its result. Setting TMPDIR environment variable after import doesn't affect the cached value. Reset tempfile.tempdir = None before tests that modify TMPDIR so the function re-evaluates the temp directory path. https://claude.ai/code/session_013sXF2YWWf8yW5hRuhGVbZU --- tests/tests.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/tests.py b/tests/tests.py index aec9a1e..705d2ac 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -25,9 +25,10 @@ class TestRuntimeDir(unittest.TestCase): """ def setUp(self): self.tmpdir = tempfile.mkdtemp() - # Save original environment + # Save original environment and tempfile cache self.orig_xdg_runtime = os.environ.get('XDG_RUNTIME_DIR') self.orig_tmpdir = os.environ.get('TMPDIR') + self.orig_tempfile_tempdir = tempfile.tempdir def tearDown(self): rmtree(self.tmpdir) @@ -40,6 +41,8 @@ def tearDown(self): os.environ['TMPDIR'] = self.orig_tmpdir elif 'TMPDIR' in os.environ: del os.environ['TMPDIR'] + # Restore tempfile cache + tempfile.tempdir = self.orig_tempfile_tempdir def test_xdg_runtime_dir_used_when_set(self): """Test that $XDG_RUNTIME_DIR/keepmenu/ is used when available @@ -62,6 +65,8 @@ def test_tmpdir_fallback_when_xdg_runtime_unset(self): custom_tmpdir = os.path.join(self.tmpdir, 'tmp') os.makedirs(custom_tmpdir, mode=0o777) os.environ['TMPDIR'] = custom_tmpdir + # Reset tempfile cache so it picks up new TMPDIR + tempfile.tempdir = None runtime_dir = KM.get_runtime_dir() @@ -77,6 +82,8 @@ def test_tmpdir_fallback_when_xdg_runtime_dir_not_exists(self): custom_tmpdir = os.path.join(self.tmpdir, 'tmp') os.makedirs(custom_tmpdir, mode=0o777) os.environ['TMPDIR'] = custom_tmpdir + # Reset tempfile cache so it picks up new TMPDIR + tempfile.tempdir = None runtime_dir = KM.get_runtime_dir()