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.')