encryption.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. # -*- coding: utf-8 -*-
  2. """
  3. Verschlüsselung für das Trixy-Protokoll.
  4. Verwendet AES-256-GCM für sichere Kommunikation.
  5. """
  6. import os
  7. import secrets
  8. from pathlib import Path
  9. from cryptography.hazmat.primitives.ciphers.aead import AESGCM
  10. from trixy_core.utils.debug import pinfo, pdebug, perror
  11. class TrixyEncryption:
  12. """
  13. AES-256-GCM Verschlüsselung für Trixy-Kommunikation.
  14. Struktur verschlüsselter Daten:
  15. - Nonce (12 bytes)
  16. - Ciphertext (variable)
  17. - Tag (16 bytes, in Ciphertext enthalten)
  18. """
  19. NONCE_SIZE = 12
  20. KEY_SIZE = 32 # 256 bit
  21. def __init__(self, key: bytes | None = None) -> None:
  22. """
  23. Initialisiert die Verschlüsselung.
  24. Args:
  25. key: 32-byte AES-Schlüssel, oder None für neuen Schlüssel
  26. """
  27. if key is None:
  28. self._key = self.generate_key()
  29. else:
  30. if len(key) != self.KEY_SIZE:
  31. raise ValueError(f"Schlüssel muss {self.KEY_SIZE} Bytes lang sein")
  32. self._key = key
  33. self._aesgcm = AESGCM(self._key)
  34. @property
  35. def key(self) -> bytes:
  36. """Gibt den Schlüssel zurück."""
  37. return self._key
  38. @staticmethod
  39. def generate_key() -> bytes:
  40. """
  41. Generiert einen neuen zufälligen Schlüssel.
  42. Returns:
  43. 32-byte Schlüssel
  44. """
  45. return secrets.token_bytes(TrixyEncryption.KEY_SIZE)
  46. def encrypt(self, plaintext: bytes, associated_data: bytes | None = None) -> bytes:
  47. """
  48. Verschlüsselt Daten mit AES-256-GCM.
  49. Args:
  50. plaintext: Zu verschlüsselnde Daten
  51. associated_data: Optionale zusätzliche authentifizierte Daten
  52. Returns:
  53. Nonce + Ciphertext (enthält Tag)
  54. """
  55. nonce = secrets.token_bytes(self.NONCE_SIZE)
  56. ciphertext = self._aesgcm.encrypt(nonce, plaintext, associated_data)
  57. return nonce + ciphertext
  58. def decrypt(self, ciphertext: bytes, associated_data: bytes | None = None) -> bytes:
  59. """
  60. Entschlüsselt Daten mit AES-256-GCM.
  61. Args:
  62. ciphertext: Nonce + verschlüsselte Daten
  63. associated_data: Optionale zusätzliche authentifizierte Daten
  64. Returns:
  65. Entschlüsselte Daten
  66. Raises:
  67. ValueError: Bei ungültigen Daten oder Authentifizierungsfehler
  68. """
  69. if len(ciphertext) < self.NONCE_SIZE + 16: # 16 = min Tag-Größe
  70. raise ValueError("Ciphertext zu kurz")
  71. nonce = ciphertext[:self.NONCE_SIZE]
  72. encrypted_data = ciphertext[self.NONCE_SIZE:]
  73. try:
  74. return self._aesgcm.decrypt(nonce, encrypted_data, associated_data)
  75. except Exception as e:
  76. perror(f"Entschlüsselung fehlgeschlagen: {e}")
  77. raise ValueError("Entschlüsselung fehlgeschlagen") from e
  78. def save_key(self, path: str | Path) -> None:
  79. """
  80. Speichert den Schlüssel in eine Datei.
  81. Args:
  82. path: Zielpfad
  83. """
  84. file_path = Path(path)
  85. file_path.parent.mkdir(parents=True, exist_ok=True)
  86. with open(file_path, "wb") as f:
  87. f.write(self._key)
  88. # Nur für Besitzer lesbar
  89. os.chmod(file_path, 0o600)
  90. pinfo(f"Verschlüsselungsschlüssel gespeichert: {file_path}")
  91. @classmethod
  92. def load_key(cls, path: str | Path) -> "TrixyEncryption":
  93. """
  94. Lädt einen Schlüssel aus einer Datei.
  95. Args:
  96. path: Dateipfad
  97. Returns:
  98. TrixyEncryption-Instanz mit geladenem Schlüssel
  99. Raises:
  100. FileNotFoundError: Wenn die Datei nicht existiert
  101. """
  102. file_path = Path(path)
  103. if not file_path.exists():
  104. raise FileNotFoundError(f"Schlüsseldatei nicht gefunden: {file_path}")
  105. with open(file_path, "rb") as f:
  106. key = f.read()
  107. pinfo(f"Verschlüsselungsschlüssel geladen: {file_path}")
  108. return cls(key)
  109. @classmethod
  110. def load_or_create(cls, path: str | Path) -> "TrixyEncryption":
  111. """
  112. Lädt oder erstellt einen Schlüssel.
  113. Args:
  114. path: Dateipfad
  115. Returns:
  116. TrixyEncryption-Instanz
  117. """
  118. file_path = Path(path)
  119. if file_path.exists():
  120. return cls.load_key(file_path)
  121. else:
  122. instance = cls()
  123. instance.save_key(file_path)
  124. return instance