feat: Eingabeprüfung, Verschlüsselung und Zeitlimits

Strikte Validierung von Server, Freigabe, Pfaden und Mount-Optionen
(Allowlist, gesperrte Zugangsdaten-Schlüssel), Fernet-Schlüssel mit 0600,
Ed25519-Schlüsselerzeugung, Schwärzung von Geheimnissen und Dateizugriffe
mit Zeitlimit in Daemon-Threads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-10-10 18:46:50 +02:00
co-authored by Claude Opus 5.5
parent 278180c541
commit 689ddf0d94
5 changed files with 617 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
"""Fallstrick 7: Schlüsseldatei, Verschlüsselung, Schwärzung, SSH-Schlüssel."""
from __future__ import annotations
import io
import stat
from pathlib import Path
import paramiko
import pytest
from cryptography.fernet import Fernet
from netmover.crypto import (
REDACTED,
SecretBox,
SecretError,
generate_ssh_keypair,
load_or_create_key,
redact,
)
def test_key_created_with_0600_and_reused(tmp_path: Path) -> None:
path = tmp_path / "lib" / "secret.key"
key1 = load_or_create_key(path)
assert stat.S_IMODE(path.stat().st_mode) == 0o600
assert load_or_create_key(path) == key1
def test_key_mode_is_tightened(tmp_path: Path) -> None:
path = tmp_path / "secret.key"
path.write_bytes(Fernet.generate_key())
path.chmod(0o644)
load_or_create_key(path)
assert stat.S_IMODE(path.stat().st_mode) == 0o600
def test_invalid_key_file(tmp_path: Path) -> None:
path = tmp_path / "secret.key"
path.write_text("kaputt")
path.chmod(0o600)
with pytest.raises(SecretError):
load_or_create_key(path)
def test_roundtrip_and_wrong_key() -> None:
box = SecretBox(Fernet.generate_key())
token = box.encrypt("geheim")
assert "geheim" not in token
assert box.decrypt(token) == "geheim"
with pytest.raises(SecretError, match=r"secret\.key"):
SecretBox(Fernet.generate_key()).decrypt(token)
def test_redact() -> None:
text = "mount error: user scanner pw Sommer2026! failed (Sommer2026)"
out = redact(text, ["Sommer2026!", "Sommer2026", None, ""])
assert "Sommer2026" not in out
assert out.count(REDACTED) == 2
def test_ssh_keypair_usable_by_paramiko() -> None:
private, public = generate_ssh_keypair("netmover@test")
key = paramiko.Ed25519Key.from_private_key(io.StringIO(private))
assert public.startswith("ssh-ed25519 ")
assert public.split()[1] == key.get_base64()
assert public.endswith(" netmover@test")