Skip to content

How Protection Works

This topic provides a high-level overview of the PyLocket protection pipeline — from uploading your artifact to running the protected application on an end-user's machine.


Protection Pipeline Overview

flowchart LR
    A["Upload\nArtifact"] --> B["Analyze\nBytecode"]
    B --> C["Protect\nFunctions"]
    C --> D["Package\nOutput"]

Stage 1: Upload

When you run pylocket protect, the CLI:

  1. Validates the artifact type (executable, wheel, zip, etc.)
  2. Uploads the artifact to PyLocket's secure cloud storage (encrypted at rest)
  3. Creates a protection job in the queue

Stage 2: Bytecode Analysis

The protection worker:

  1. Identifies the packaging format (PyInstaller, cx_Freeze, Briefcase, wheel, or ZIP)
  2. Extracts all Python bytecode from the artifact
  3. Enumerates every function, method, and class body in the code

Module Classification

Before encrypting, PyLocket classifies each module as user code (to be protected) or dependency code (to be skipped). Only your application code is protected — standard library modules, packaging tool internals, and common third-party packages are automatically excluded. You can override the classification with the PYLOCKET_FORCE_INCLUDE and PYLOCKET_FORCE_EXCLUDE environment variables. See Module Selection for details.

Stage 3: Function-Level Protection

Each discovered function is individually encrypted using PyLocket's proprietary protection pipeline. The original function body is replaced with a lightweight stub that delegates to the secure native runtime. This process ensures that the original logic is never present in plaintext within the distributed artifact.

Stage 4: Packaging

The protected output is assembled:

  1. The modified artifact (with stub-replaced functions)
  2. The encrypted function data
  3. A signed protection manifest
  4. The native runtime library
  5. Bootstrap code (injected into the application's startup path)

Stub Replacement

Original Python function:

def calculate(x, y):
    result = x ** 2 + y ** 2
    return math.sqrt(result)

After protection, a decompiler sees only a lightweight stub that delegates to the native runtime. The original logic is completely absent.

The stub: - Preserves the function signature (name, arguments, decorators) - Replaces only the function body - Delegates to the native runtime using the function's unique ID - Reveals nothing about the original logic

Each protected build uses randomized stub variations, so no two builds have the same stub fingerprint. Automated tools cannot pattern-match stubs across different builds.


Runtime Execution

When a protected application launches:

  1. Bootstrap runs automatically before any user code
  2. The bootstrap validates the license — either from a cached activation or by contacting the PyLocket activation service. The license key is obtained from the user prompt or the PYLOCKET_LICENSE_KEY environment variable.
  3. On successful validation, the bootstrap initializes the native runtime with the received key material
  4. When a stub function is called, the native runtime decrypts the function on demand, executes it, and securely zeroes the plaintext from memory

The master key is never embedded in the distributed artifact. Key material comes exclusively from the license activation service, ensuring that even full access to the artifact is insufficient for decryption without a valid license.

The native runtime handles all decryption, integrity verification, and security checks transparently. From the application's perspective, protected functions behave identically to their unprotected originals.


What Gets Protected (and What Doesn't)

Protected

  • All Python function bodies (functions, methods, lambdas, comprehensions)
  • Class bodies
  • Module-level code (the __init__ code object)

Not Protected

  • Non-Python files (images, configs, data files, HTML templates)
  • C extension modules (.pyd, .so files compiled from C/Cython)
  • Function signatures (argument lists remain in stubs so calls still work)
  • Module names and directory structure
  • __pycache__ files (these are recreated by the stubs)
  • Scopes that cannot be encrypted without breaking the app, or where there is nothing to gain: bodies carrying free variables from an enclosing function (Python's exec() rejects those outright), generator and coroutine bodies, bodies too small to carry information, and bodies with no names or string constants to hide. Packaging shapes where sealing cannot be applied safely, which today includes wheels and other Python package layouts, are also left unsealed. Every one of these is reported in the build's coverage summary rather than skipped silently.

The limit worth being explicit about

Encryption keeps your class attributes, constants and docstrings out of the files you ship. It does not hide them from a licensed user at runtime: the application must be able to read its own values, so getattr returns the real credential while the app runs.

The guarantee is strong against anyone who does not hold a valid licence, and it raises the cost substantially for anyone who does. It is not a secret store. Anything your customer must never learn belongs on your server, not in the binary.

Note: If you need to protect non-Python data, encrypt it at the application level and decrypt it in your protected Python code.


See Also