Files
Athena-Deck/backup_codec.py
T

33 lines
1.9 KiB
Python

"""Versioned, authenticated backup envelope; no archive extraction or executable pickle."""
import hashlib,json,secrets,zlib
MAGIC=b'ATHENA-DECK-BACKUP\x01\n'
MAX=64*1024*1024
def password(value):
if not isinstance(value,str) or not 16<=len(value)<=256:raise ValueError('Backup-Kennwort: 16 bis 256 Zeichen erforderlich.')
return value.encode()
def seal(document,phrase):
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
salt=secrets.token_bytes(16);nonce=secrets.token_bytes(12);head=MAGIC+salt+nonce
key=hashlib.scrypt(password(phrase),salt=salt,n=32768,r=8,p=1,maxmem=64*1024*1024,dklen=32)
raw=json.dumps(document,ensure_ascii=False,allow_nan=False,separators=(',',':')).encode()
if len(raw)>MAX:raise ValueError('Konfigurationsbackup überschreitet 64 MiB.')
return head+AESGCM(key).encrypt(nonce,zlib.compress(raw),head)
def open_backup(raw,phrase):
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag
if not isinstance(raw,bytes) or not len(MAGIC)+44<=len(raw)<=MAX or not raw.startswith(MAGIC):raise ValueError('Kein unterstütztes Athena-Deck-Backup.')
salt=raw[len(MAGIC):len(MAGIC)+16];nonce=raw[len(MAGIC)+16:len(MAGIC)+28];head=raw[:len(MAGIC)+28]
key=hashlib.scrypt(password(phrase),salt=salt,n=32768,r=8,p=1,maxmem=64*1024*1024,dklen=32)
try:packed=AESGCM(key).decrypt(nonce,raw[len(head):],head)
except InvalidTag:raise ValueError('Backup-Kennwort falsch oder Backup beschädigt.') from None
try:
decoder=zlib.decompressobj();data=decoder.decompress(packed,MAX+1)
if len(data)>MAX or decoder.unconsumed_tail or not decoder.eof or decoder.unused_data:raise ValueError()
result=json.loads(data,parse_constant=lambda _:(_ for _ in ()).throw(ValueError()))
except (ValueError,zlib.error,UnicodeError):raise ValueError('Backup-Inhalt ungültig oder zu groß.') from None
if not isinstance(result,dict):raise ValueError('Ungültiges Backup.')
return result