PyCryption Research Tutorial

This notebook serves a technical introduction, demonstration, and collaboration baseline for this repository, PyCryption. It values ‘fire-and-forget’ systems to allow you to write, and test custom encryption systems.

The true way to measure performance would be analyze mathematical complexity, but this is a good baseline for understanding the performance of your system, especially in production, and prototyping.

Some good references I enjoyed learning from:


0.0 - Getting Started: Building a Basic Algorithm

In this section we’ll exploring how to rapidly test new encryption algorithms without writing tedious harnesses for testing.

0.1 Understand the Mental Model

  1. Define your encryption algorithm into a class with two endpoint functions: encrypt, and decrypt, that can accept data, and the algorithm context.
  2. Perform the quick_test to ensure successful dataflow.
  3. Register other models and algorithms to compare.
  4. Refine and/or repeat.

0.2 Encryption Helpers and Algorithm Context

An incredibly powerful component, AlgorithmContext is a communication interface to your algorithm from the cryptography components you add to your algorithm.

from lib.notebook import (
    algorithm,
    with_key,
    generate_key,
    AlgorithmContext,
    ComposerSession,
    ReportBuilder,
    quick_test,
    with_memory_profiling,
)

# Initialize report builder for styled output
report = ReportBuilder()

# -----------------------------------------------------------------------------
# Prototype Algorithm Development
# -----------------------------------------------------------------------------
# Use decorators for logistics (key injection, context, metrics).
# Write your own experimental crypto logic in the methods.

KEY = generate_key(32)


@algorithm("XOR-Prototype")
@with_key(KEY)
@with_memory_profiling()
class XORPrototype:
    """
    Simple, insecure XOR-based prototype for testing the framework.
    """

    def encrypt(self, data: bytes, ctx: AlgorithmContext) -> bytes:
        # Prototype: simple repeating-key XOR
        key = ctx.key
        return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))

    def decrypt(self, data: bytes, ctx: AlgorithmContext) -> bytes:
        # XOR is symmetric
        key = ctx.key
        return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))


# Verify the prototype works
quick_test(XORPrototype())
Testing: XOR-Prototype
Input: b'Hello, PyCryption!'
----------------------------------------
Encrypt: <AlgorithmResult: 18 bytes, 0.676ms>
Decrypt: <AlgorithmResult: 18 bytes, 0.021ms>
Round-trip successful!
# -----------------------------------------------------------------------------
# Composer Session - Benchmark Prototype vs Production Algorithms
# -----------------------------------------------------------------------------

from lib.notebook import adapt
from lib.algorithms import Aes256GcmAlgorithm

# Create session and register algorithms
session = ComposerSession()
session.register(XORPrototype())
session.register(adapt(Aes256GcmAlgorithm, KEY, name="AES-256-GCM", profile_memory=True))

report.info(f"Registered: {session.list_algorithms()}")

# Test all registered algorithms
report.heading("Round-trip Tests", level=2)
report.test_results(session.test_all())
 Registered: ['XOR-Prototype', 'AES-256-GCM']
Round-trip Tests
     Round-trip Tests     
┏━━━━━━━━━━━━━━━┳━━━━━━━━┓
 Algorithm      Status 
┡━━━━━━━━━━━━━━━╇━━━━━━━━┩
 XOR-Prototype  ✓ PASS 
 AES-256-GCM    ✓ PASS 
└───────────────┴────────┘
# -----------------------------------------------------------------------------
# Benchmark Prototype Performance
# -----------------------------------------------------------------------------

report.heading("Prototype Benchmark (10KB, 50 iterations)", level=2)
report.comparison_table(session.compare(data_size=10_000, iterations=50))
Prototype Benchmark (10KB, 50 iterations)
                                               Algorithm Comparison                                                
┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━┓
                                            Throughput               P99 Encrypt                          
 Algorithm      Encrypt (ms)  Decrypt (ms)  (MB/s)         Ops/sec   (ms)          Expansion  Peak Memory 
┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━┩
 AES-256-GCM    0.041         0.039         243.9          24390.24  0.103         1.0016     10.6 KB     
 XOR-Prototype  5.92          5.855         1.69           168.92    10.972        1.0        11.8 KB     
└───────────────┴──────────────┴──────────────┴───────────────┴──────────┴──────────────┴───────────┴─────────────┘
# -----------------------------------------------------------------------------
# Detailed Benchmarks - Scaling Analysis
# -----------------------------------------------------------------------------

report.heading("Scaling Analysis", level=2)
report.benchmark_table(
    session.benchmark_all(data_sizes=[100, 1_000, 10_000, 100_000], iterations=20)
)
Scaling Analysis
                            XOR-Prototype                             
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
 Size       Encrypt (ms)  Decrypt (ms)  Throughput  Peak Memory 
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
 100 B             0.059         0.052   1.69 MB/s        501 B 
 1,000 B            0.83          0.86    1.2 MB/s       1.4 KB 
 10,000 B          6.233         6.086    1.6 MB/s      11.8 KB 
 100,000 B         64.65        65.131   1.55 MB/s     106.9 KB 
└───────────┴──────────────┴──────────────┴────────────┴─────────────┘
 Scaling factor (large/small throughput): 0.917x

                              AES-256-GCM                               
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
 Size       Encrypt (ms)  Decrypt (ms)    Throughput  Peak Memory 
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
 100 B             0.039         0.035     2.56 MB/s        898 B 
 1,000 B           0.032         0.029    31.25 MB/s       1.8 KB 
 10,000 B          0.043         0.038   232.56 MB/s      10.6 KB 
 100,000 B         0.073         0.063  1369.86 MB/s      98.5 KB 
└───────────┴──────────────┴──────────────┴──────────────┴─────────────┘
 Scaling factor (large/small throughput): 535.102x

# -----------------------------------------------------------------------------
# Session Metrics - Aggregated Statistics
# -----------------------------------------------------------------------------

report.heading("Session Report", level=2)
report.session_report(session.report())
Session Report
                                               Session Report                                                
┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
 Algorithm      Operations  Avg Encrypt (ms)  Avg Decrypt (ms)  Total Bytes  Errors  Avg Peak Memory 
┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
 XOR-Prototype  1E / 1D     1.203             0.041             36           -       419 B           
 AES-256-GCM    1E / 1D     1.294             0.051             52           -       8.1 KB          
└───────────────┴────────────┴──────────────────┴──────────────────┴─────────────┴────────┴─────────────────┘

1.0: Adding Cryptographic Capabilities

Beyond a simple byte emitter in the last example, we can easily add new capabilities. Let’s take a look at the Salsa20 algorithm which was succeeded by its descendent, ChaCha in 2008. Though it’s dated, it still uses much of the same IO of modern ciphers.

import hashlib
from Crypto.Cipher import Salsa20
from lib.notebook import with_metrics


def salsa_kdf(key: bytes, salt: bytes) -> bytes:
    return hashlib.pbkdf2_hmac("sha256", key, salt, 100000, dklen=32)


@algorithm("Salsa20-Prototype")
@with_key(KEY)
@with_metrics()
@with_memory_profiling()
class Salsa20Prototype:
    """
    A classic encryption cipher, Salsa20 was shelved in 2008.
    """

    def encrypt(self, data: bytes, ctx: AlgorithmContext) -> bytes:
        # Register KDF and salt, derive key, cache result
        ctx.set_kdf("salsa-kdf", salsa_kdf)
        ctx.set_salt("salsa-salt")

        # extract a unique key for this encryption from the original key (KDF + salt) and store/cache it
        derived = ctx.derive("salsa-kdf", "salsa-salt", cache_as="salsa-derived")

        cipher = Salsa20.new(derived)
        ctx.set_nonce("salsa-nonce", cipher.nonce)
        return cipher.encrypt(data)

    def decrypt(self, data: bytes, ctx: AlgorithmContext) -> bytes:
        # Retrieve cached materials from registry
        derived = ctx.get_derived_key("salsa-derived")
        assert derived is not None, "Derived key not found in context"
        nonce = ctx.get_nonce("salsa-nonce")
        assert nonce is not None, "Nonce not found in context"

        # create cipher and decrypt
        cipher = Salsa20.new(derived, nonce)
        return cipher.decrypt(data)


# Verify the prototype works
quick_test(Salsa20Prototype())
# load the Salsa20 cipher with the password KDF into our active session
session.register(Salsa20Prototype(), "Salsa20-Prototype")
# load the AES-256-GCM cipher with memory profiling
session.register(adapt(Aes256GcmAlgorithm, KEY, name="AES-256-GCM", profile_memory=True))
Testing: Salsa20-Prototype
Input: b'Hello, PyCryption!'
----------------------------------------
Encrypt: <AlgorithmResult: 18 bytes, 35.036ms>
Decrypt: <AlgorithmResult: 18 bytes, 0.089ms>
Round-trip successful!
<lib.notebook.composer.ComposerSession at 0x7a6748d22620>

1.1 Modernizing the Prototype: Upping the Ante

Salsa20 should never be used in today’s systems, it’s legacy tech but deserves its rightful place in history.

Let’s take a look at it’s direct

report.heading("Scaling Analysis", level=2)
report.benchmark_table(
    session.benchmark_all(data_sizes=[100, 1_000, 10_000, 100_000], iterations=20)
)
Scaling Analysis
                            XOR-Prototype                             
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
 Size       Encrypt (ms)  Decrypt (ms)  Throughput  Peak Memory 
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
 100 B             0.159         0.219   0.63 MB/s        556 B 
 1,000 B           0.855         0.864   1.17 MB/s       1.4 KB 
 10,000 B          6.595          6.78   1.52 MB/s      11.8 KB 
 100,000 B        66.544        66.868    1.5 MB/s     106.9 KB 
└───────────┴──────────────┴──────────────┴────────────┴─────────────┘
 Scaling factor (large/small throughput): 2.381x

                              AES-256-GCM                               
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
 Size       Encrypt (ms)  Decrypt (ms)    Throughput  Peak Memory 
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
 100 B             0.057         0.036     1.75 MB/s        900 B 
 1,000 B           0.045         0.039    22.22 MB/s       1.8 KB 
 10,000 B          0.046         0.042   217.39 MB/s      10.6 KB 
 100,000 B         0.065         0.061  1538.46 MB/s      98.5 KB 
└───────────┴──────────────┴──────────────┴──────────────┴─────────────┘
 Scaling factor (large/small throughput): 879.12x

                          Salsa20-Prototype                           
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
 Size       Encrypt (ms)  Decrypt (ms)  Throughput  Peak Memory 
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
 100 B            29.391         0.049    0.0 MB/s        522 B 
 1,000 B          28.157         0.043   0.04 MB/s       1.4 KB 
 10,000 B          28.61         0.074   0.35 MB/s      10.2 KB 
 100,000 B         28.26         0.343   3.54 MB/s      98.1 KB 
└───────────┴──────────────┴──────────────┴────────────┴─────────────┘

1.1 Analyzing our Results

Comparing Salsa20, and AES is inherently unfair and is equivalent of comparing a 2016 NASCAR vs Steve McQueen’s Ferrari. Salsa20 is a stream cipher, and AES is a block cipher. Stream ciphers are much more lightweight, and perform impeccably against dynamic-sized data blocks, hence the ‘stream’ part of the cipher.

Let’s play into this, despite the lack of realism, and go-through the motions of selecting an algorithm from research done using this composer. 256-bit AES-GCM clearly wins the battle for scalability with the ability to encrypt gigabytes at a time whilst still remaining around and even lower in memory than the Salsa20 cipher, and the XOR prototype (binary additive stream cipher) that we built; which wouldn’t have even stood the test of WWII.

Back to top