QUELLCODE / PY
privacy_check.py
project-site/privacy_check.py
1# OpenAss: nur privat und experimentell. Bau, Download und Nutzung auf eigene Verantwortung.2# Niemals Unternehmensrechner, Produktivsysteme, Industrieanlagen oder sicherheitskritische Systeme anschließen oder bedienen.3# Keine freiwillige Funktions-/Sicherheitsgarantie; zwingende gesetzliche Ansprüche bleiben unberührt. Siehe DISCLAIMER.txt.4"""Fail a public export on personal paths, credential signatures or image metadata.56This is a release guard, not a proof that arbitrary files contain no sensitive data.7Diagnostics identify the file and category without printing matching values.8"""9from __future__ import annotations1011import gzip12import hashlib13import io14import re15import struct16import sys17import zipfile18import zlib19from pathlib import Path2021HOME_PATH = re.compile(rb'/(?:Users|home)/([^/\s\x00<>"\']+)', re.I)22WINDOWS_HOME = re.compile(rb'[A-Z]:[\\/](?:Users|Documents and Settings)[\\/][^\\/\s\x00]+', re.I)23SIGNATURES = {24 'private key': rb'-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY-----',25 'OpenAI key': rb'\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{20,}',26 'GitHub token': rb'\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{40,})',27 'GitLab token': rb'\bglpat-[A-Za-z0-9_-]{20,}',28 'AWS key': rb'\b(?:AKIA|ASIA)[A-Z0-9]{16}\b',29 'Google key': rb'\bAIza[A-Za-z0-9_-]{35}\b',30 'Slack token': rb'\bxox[baprs]-[0-9A-Za-z-]{20,}',31 'credential in URL': rb'https?://[^/\s:@"<>]+:[^/\s@"<>]+@',32}33PATTERNS = {kind: re.compile(pattern) for kind, pattern in SIGNATURES.items()}34PNG_MAGIC = b'\x89PNG\r\n\x1a\n'35ZSTD_MAGIC = bytes.fromhex('28b52ffd')36_checked: set[tuple[str, bool]] = set()373839def _personal_patterns() -> list[bytes]:40 home = Path.home()41 values = [home.as_posix().encode()]42 if len(home.name) >= 6:43 values.append(home.name.encode())44 return values454647def blend_payload(data: bytes) -> bytes:48 if data.startswith(ZSTD_MAGIC):49 try:50 from compression import zstd51 return zstd.decompress(data)52 except ImportError:53 try:54 import zstandard55 except ImportError as error:56 raise RuntimeError('Compressed Blender audit requires Python 3.14 or zstandard; install requirements-build.txt.') from error57 with zstandard.ZstdDecompressor().stream_reader(io.BytesIO(data)) as reader:58 return reader.read()59 if data.startswith(b'\x1f\x8b'):60 return gzip.decompress(data)61 return data626364def _reject(label: str, category: str) -> None:65 raise ValueError(f'Public export blocked: {label} [{category}]')666768def assert_public_bytes(label: str, data: bytes, depth: int = 0) -> None:69 if depth > 4:70 _reject(label, 'excessively nested archive')71 is_blend = label.lower().endswith('.blend')72 fingerprint = (hashlib.sha256(data).hexdigest(), is_blend)73 if fingerprint in _checked:74 return75 payload = blend_payload(data) if is_blend else data76 if is_blend and not payload.startswith(b'BLENDER'):77 _reject(label, 'unrecognized Blender format')78 for match in HOME_PATH.finditer(payload):79 # The installation guide's generic Raspberry Pi account is intentional.80 if match.group(1) != b'pi':81 _reject(label, 'personal home-directory path')82 if WINDOWS_HOME.search(payload):83 _reject(label, 'personal Windows path')84 lower = payload.lower()85 for value in _personal_patterns():86 if value.lower() in lower:87 _reject(label, 'local user identifier')88 for kind, pattern in PATTERNS.items():89 if pattern.search(payload):90 _reject(label, kind)91 if data.startswith(PNG_MAGIC):92 offset = len(PNG_MAGIC)93 while offset < len(data):94 if offset + 12 > len(data):95 _reject(label, 'invalid PNG chunk')96 size = struct.unpack_from('>I', data, offset)[0]97 kind = data[offset + 4:offset + 8]98 end = offset + 12 + size99 if end > len(data):100 _reject(label, 'invalid PNG length')101 chunk = data[offset + 4:offset + 8 + size]102 crc = struct.unpack_from('>I', data, offset + 8 + size)[0]103 if zlib.crc32(chunk) & 0xffffffff != crc:104 _reject(label, 'invalid PNG checksum')105 if kind in {b'tEXt', b'zTXt', b'iTXt', b'eXIf'}:106 _reject(label, 'PNG text or EXIF metadata')107 offset = end108 if kind == b'IEND':109 break110 if data.startswith(b'\xff\xd8') and (b'Exif\x00\x00' in data or b'http://ns.adobe.com/xap/' in data):111 _reject(label, 'JPEG EXIF or XMP metadata')112 if zipfile.is_zipfile(io.BytesIO(data)):113 with zipfile.ZipFile(io.BytesIO(data)) as archive:114 for member in archive.infolist():115 if not member.is_dir():116 assert_public_bytes(label + '!' + member.filename, archive.read(member), depth + 1)117 _checked.add(fingerprint)118119120def check_tree(directory: Path) -> int:121 files = sorted(path for path in directory.rglob('*') if path.is_file())122 for path in files:123 if path.is_symlink():124 _reject(path.relative_to(directory).as_posix(), 'symlink')125 assert_public_bytes(path.relative_to(directory).as_posix(), path.read_bytes())126 return len(files)127128129if __name__ == '__main__':130 directory = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent / 'public'131 print(f'Privacy guard passed: {check_tree(directory)} public files, including archive payloads.')