"""Test that cmail-setup-finish decrypts the mail password without a world-readable window. gpg creates its --output file at the process umask (typically 0644), so a plain `gpg --decrypt --output f` followed by `chmod 600 f` leaves the plaintext mail password world-readable in the gap between the two. decrypt_to_secure wraps the decrypt in a 0077 umask subshell so the file is 0600 from creation. Method: sed-extract decrypt_to_secure from scripts/cmail-setup-finish.sh, run it with a fake gpg that (a) records the effective umask at call time and (b) writes the --output file the way real gpg would (respecting the umask). Assert the umask was tight during the write, and that the resulting file is 0600. Run from repo root: python3 -m unittest tests.cmail.test_cmail_setup """ import os import stat import subprocess import tempfile import textwrap import unittest REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) SCRIPT = os.path.join(REPO_ROOT, "scripts", "cmail-setup-finish.sh") def run_decrypt(): """Run decrypt_to_secure with a fake gpg. Returns (plain_mode, umask_str).""" tmp = tempfile.mkdtemp() enc = os.path.join(tmp, "in.gpg") plain = os.path.join(tmp, "cmailpass") umask_rec = os.path.join(tmp, "umask") with open(enc, "w") as fh: fh.write("ciphertext") script = textwrap.dedent(f"""\ UMASK_REC={umask_rec!r} gpg() {{ umask > "$UMASK_REC" local out="" while [ $# -gt 0 ]; do case "$1" in --output) out="$2"; shift;; esac shift done printf 'SECRET' > "$out" }} source <(sed -n '/^decrypt_to_secure() {{/,/^}}/p' {SCRIPT!r}) decrypt_to_secure {enc!r} {plain!r} """) subprocess.run(["bash", "-c", script], check=True, capture_output=True, text=True, timeout=10) mode = stat.S_IMODE(os.stat(plain).st_mode) with open(umask_rec) as fh: umask_str = fh.read().strip() return mode, umask_str class CmailDecryptPermissions(unittest.TestCase): def test_decrypt_runs_under_tight_umask(self): # The security property: gpg writes the plaintext under a 0077 umask, so # it is 0600 from creation, not world-readable before the chmod. _mode, umask_str = run_decrypt() self.assertEqual( int(umask_str, 8), 0o077, "the mail-password decrypt must run under a 0077 umask so the " "plaintext is never world-readable", ) def test_plaintext_is_owner_only(self): mode, _umask = run_decrypt() self.assertEqual(mode, 0o600, f"cmailpass ended at {oct(mode)}, expected 0600") self.assertFalse(mode & 0o077, "cmailpass must not be group- or world-accessible") if __name__ == "__main__": unittest.main()