Le problème PyPI

Distribuer un package via PyPI rend le code source accessible à tous — pip télécharge et installe les fichiers .py en clair. Pour un package commercial ou contenant de la propriété intellectuelle, cette transparence est incompatible avec la protection du code.

Trois options existent : distribuer uniquement des binaires compilés (wheels sans source), utiliser des extensions C (opacité partielle), ou chiffrer le code Python et fournir un loader de déchiffrement. C'est cette troisième approche qu'IronLock rend viable.

PyPI n'est pas conçu pour distribuer du code propriétaire — mais avec les bons mécanismes, il peut servir de canal de distribution sécurisé pour des packages commerciaux.

Approches possibles

  • Wheel binaire only (bdist_wheel) — distribuer uniquement le .pyc compilé, sans .py. Résistance : faible (décompilateurs Python restaurent le source).
  • Extension C (Cython) — compiler en extension .so/.pyd. Résistance : moyenne (reverse engineering C possible mais lent).
  • Code chiffré + loader IronLock — le package distribue des fichiers .ironenc et un loader Python. Résistance : haute si combiné avec anti-debug.

Structure du wheel protégé

# Structure du wheel monpackage-1.1.0-py3-none-any.whl
monpackage/
├── __init__.py          # loader uniquement (pas de logique)
├── _protected/
│   ├── core.ironenc     # logique métier chiffrée
│   ├── utils.ironenc
│   └── manifest.json    # SHA-256 des .ironenc
├── _loader/
│   ├── decrypt.py       # moteur de déchiffrement
│   └── public_key.pem   # clé publique ECDSA
└── monpackage-1.1.0.dist-info/

Hook d'import

Python permet de personnaliser le mécanisme d'import via sys.meta_path. On installe un finder/loader qui déchiffre les modules à la volée :

# monpackage/__init__.py
import sys
from ._loader.decrypt import IronLockImporter

# Installer le hook d'import au chargement du package
_importer = IronLockImporter(
    protected_dir=f"{__path__[0]}/_protected",
    public_key_path=f"{__path__[0]}/_loader/public_key.pem"
)
sys.meta_path.insert(0, _importer)

# Le reste du __init__.py importe normalement
from .core import MainClass  # → déchiffrement transparent
# _loader/decrypt.py
import importlib.abc, importlib.machinery
from pathlib import Path

class IronLockImporter(importlib.abc.MetaPathFinder, importlib.abc.Loader):
    def __init__(self, protected_dir, public_key_path):
        self.protected = Path(protected_dir)
        self.pubkey    = load_public_key(public_key_path)

    def find_spec(self, fullname, path, target=None):
        enc_path = self.protected / f"{fullname.split('.')[-1]}.ironenc"
        if enc_path.exists():
            return importlib.machinery.ModuleSpec(fullname, self)
        return None

    def create_module(self, spec): return None

    def exec_module(self, module):
        enc_path = self.protected / f"{module.__name__.split('.')[-1]}.ironenc"
        source = decrypt_ironenc(enc_path, self.pubkey)
        exec(compile(source, str(enc_path), 'exec'), module.__dict__)

Vérification de licence à l'utilisation

# Dans le __init__.py — vérification avant import des modules protégés
from ._loader.decrypt import IronLockImporter, verify_licence

def _check_licence():
    lic_path = Path.home() / ".monpackage" / "licence.lic"
    if not lic_path.exists():
        raise RuntimeError(
            "Licence monpackage non trouvée. "
            "Exécutez : python -m monpackage.activate"
        )
    verify_licence(lic_path, get_hardware_fingerprint())

_check_licence()  # exécuté à chaque import

Pipeline de build

# build_protected_wheel.sh
# 1. Chiffrer les modules Python
ironlock encrypt src/monpackage/   --output monpackage/_protected/   --pubkey keys/public.pem

# 2. Construire le wheel
python -m build --wheel

# 3. Signer le wheel
twine check dist/*.whl
gpg --detach-sign dist/monpackage-1.1.0-py3-none-any.whl

Limites et alternatives

  • Limite principale : le hook d'import lui-même est en Python non chiffré — un attaquant peut le modifier. Mitigation : le hook vérifie sa propre intégrité via HMAC.
  • Alternative PyPI privé : héberger un index PyPI privé (Artifactory, Nexus, pip-server) accessible uniquement aux clients authentifiés. Le code n'est alors pas public.
  • Alternative distribution directe : contourner PyPI et distribuer le wheel directement par lien signé (presigned S3 URL avec TTL court).

Conclusion

Distribuer un package protégé via PyPI est possible avec un hook d'import personnalisé et des modules chiffrés .ironenc. La protection n'est pas hermétique (le loader est visible), mais elle résiste à l'extraction automatisée et force un effort significatif de reverse engineering. Pour une protection maximale, combiner avec un PyPI privé authentifié.

🔐
PRODUIT LIÉ
IronLock v2.0
← Article précédent Article suivant →