Skip to main content

Custom Layers: Building Your Own Lock

Mental Model: Creating Your Own Lock

Imagine you want a special lock for your treasure chest. You can:

  1. Buy a standard lock (like the provided AES/DH/Signal/MLS/ML-KEM layers)
  2. Build your own custom lock (implement the CipherLayer interface)

CascadingCipher is designed to be extensible—you can create your own cipher layers with any encryption algorithm!

Why Custom Layers?

🎯 Your Custom Encryption Needs

Maybe you want:

  1. Performance: Use faster algorithms like ChaCha20
  2. Niche use cases: Homomorphic encryption for computation
  3. Legacy systems: Integrate with existing encryption
  4. Research: Test new cryptographic algorithms
  5. Compliance: Meet specific regulatory requirements

🛡️ Flexibility

The CascadingCipher framework treats all layers the same—if you implement the interface correctly, it works!


The CipherLayer Interface

Every layer must implement this interface:

interface CipherLayer {
// Layer name (for metadata)
name: string

// Encrypt data
encrypt(data: Uint8Array): Promise<Uint8Array>

// Decrypt data
decrypt(data: Uint8Array): Promise<Uint8Array>

// (Optional) Validate configuration
validateKeys?(): Promise<boolean>
}

That's it! Just three methods (plus optional validation).


Example: ChaCha20-Poly1305 Layer

ChaCha20 is a fast stream cipher that's 2-3x faster than AES-GCM. Let's build a layer for it!

Step 1: Define the Layer

import { CipherLayer } from '@cascading-cipher/core'

interface ChaChaConfig {
key: Uint8Array // 32 bytes
}

export class ChaChaLayer implements CipherLayer {
name = 'ChaCha20-Poly1305'
private config: ChaChaConfig

constructor(config: ChaChaConfig) {
this.config = config
}

async encrypt(data: Uint8Array): Promise<Uint8Array> {
// 1. Generate a random nonce (12 bytes)
const nonce = crypto.getRandomValues(new Uint8Array(12))

// 2. Import the key
const key = await crypto.subtle.importKey(
'raw',
this.config.key,
'chacha20-poly1305',
false,
['encrypt', 'decrypt']
)

// 3. Encrypt with ChaCha20-Poly1305
const encrypted = await crypto.subtle.encrypt(
{ name: 'chacha20-poly1305', nonce },
key,
data
)

// 4. Return nonce + ciphertext
return new Uint8Array([...nonce, ...new Uint8Array(encrypted)])
}

async decrypt(data: Uint8Array): Promise<Uint8Array> {
// 1. Extract nonce (first 12 bytes)
const nonce = data.slice(0, 12)
const ciphertext = data.slice(12)

// 2. Import the key
const key = await crypto.subtle.importKey(
'raw',
this.config.key,
'chacha20-poly1305',
false,
['encrypt', 'decrypt']
)

// 3. Decrypt
const decrypted = await crypto.subtle.decrypt(
{ name: 'chacha20-poly1305', nonce },
key,
ciphertext
)

return new Uint8Array(decrypted)
}

async validateKeys(): Promise<boolean> {
// Validate that the key is 32 bytes
return this.config.key.length === 32
}
}

Step 2: Use Your Custom Layer

// Generate a ChaCha20 key
const chaChaKey = crypto.getRandomValues(new Uint8Array(32))

// Create a manager with your custom layer
const manager = new CascadingCipherManager()
await manager.addLayer(new ChaChaLayer({ key: chaChaKey }))

// Encrypt
const plaintext = 'Fast encryption with ChaCha20!'
const encrypted = await manager.encrypt(plaintext)

// Decrypt
const decrypted = await manager.decrypt(encrypted)
console.log('Decrypted:', decrypted)
// "Fast encryption with ChaCha20!"

Example: Homomorphic Encryption Layer

Homomorphic encryption allows computations on encrypted data! Let's create a layer for a simplified version.

Warning: This is Educational Only!

Real homomorphic encryption (like CKKS/BFV) is complex. This is a simplified example!

interface PaillierKeyPair {
publicKey: {
n: bigint
g: bigint
}
privateKey: {
lambda: bigint
mu: bigint
n: bigint
}
}

export class PaillierLayer implements CipherLayer {
name = 'Paillier Homomorphic'
private keyPair: PaillierKeyPair

constructor(keyPair: PaillierKeyPair) {
this.keyPair = keyPair
}

async encrypt(data: Uint8Array): Promise<Uint8Array> {
// Simple Paillier encryption (educational example)
// Real implementation would use a proper library

const message = new BigInt(data)
const { n, g } = this.keyPair.publicKey
const m = message % n

// Encryption: c = g^m * r^n mod n^2
const r = BigInt(Math.random() * Number(n)) // Random r
const c = pow(g, m) * pow(r, n) % pow(n, 2n)

return new Uint8Array(Uint8Array.from(c.toString(16), c => parseInt(c, 16)))
}

async decrypt(data: Uint8Array): Promise<Uint8Array> {
// Paillier decryption
const c = BigInt(new Array(data))

const n = this.keyPair.privateKey.n
const lambda = this.keyPair.privateKey.lambda
const mu = this.keyPair.privateKey.mu

// Decryption: m = (L(c^lambda mod n^2) * mu) mod n
const m = (pow(c, lambda) % pow(n, 2n) - 1n) / n * mu % n

return new Uint8Array(m.toString())
}
}

Note: Don't use this in production! Use a library like node-paillier or Microsoft SEAL.


Example: Compression Layer (Non-Encryption)

Layers don't have to be encryption—they can be any transformation!

export class CompressionLayer implements CipherLayer {
name = 'GZIP Compression'

async encrypt(data: Uint8Array): Promise<Uint8Array> {
// Compress the data
const compressed = new gzipEncode(data)
return compressed
}

async decrypt(data: Uint8Array): Promise<Uint8Array> {
// Decompress the data
const decompressed = new gzipDecode(data)
return decompressed
}
}

Using Compression

// Add compression before encryption
const manager = new CascadingCipherManager()
await manager.addLayer(new CompressionLayer()) // Compress first
await manager.addLayer(new AESCipherLayer({ password: 'secret' }))

// Encrypt: compress → encrypt → smaller ciphertext!
const encrypted = await manager.encrypt(message)

// Decrypt: decompress → original message
const decrypted = await manager.decrypt(encrypted)

Example: Legacy Integration

Maybe you have an existing encryption library you want to use:

import { LegacyEncryptor } from '@my-company/legacy-crypto'

export class LegacyLayer implements CipherLayer {
name = 'Legacy V1'
private legacy: LegacyEncryptor

constructor() {
this.legacy = new LegacyEncryptor()
}

async encrypt(data: Uint8Array): Promise<Uint8Array> {
// Use the legacy encryptor
const encrypted = this.legacy.encrypt_v1(data)
return encrypted
}

async decrypt(data: Uint8Array): Promise<Uint8Array> {
// Use the legacy decryptor
const decrypted = this.legacy.decrypt_v1(data)
return decrypted
}
}

Gradual Migration

// Phase 1: Add legacy layer
const manager = new CascadingCipherManager()
await manager.addLayer(new LegacyLayer())
await manager.addLayer(new AESCipherLayer())

// Phase 2: Remove legacy layer
// (After migration to the new system)
const newManager = new CascadingCipherManager()
await newManager.addLayer(new AESCipherLayer())
await newManager.addLayer(new DHCipherLayer(...))

Best Practices for Custom Layers

✅ DO

  1. Implement all three methods (encrypt, decrypt, validateKeys)
  2. Test thoroughly with known plaintext/ciphertext
  3. Document your layer (what algorithm, what parameters)
  4. Handle errors gracefully (return clear error messages)
  5. Use established libraries (don't roll your own crypto!)

🚫 DON'T

  1. Create your own cryptographic primitives (no custom DH/AES!)
  2. Skip validation (validateKeys is essential)
  3. Make layers stateful (avoid storing data between encrypt/decrypt)
  4. Ignore error handling (return detailed errors)
  5. Deploy without review (get expert cryptographic review!)

Testing Your Layer

async function testCustomLayer() {
const layer = new ChaChaLayer({
key: crypto.getRandomValues(new Uint8Array(32))
})

// Test encryption/decryption
const plaintext = new TextEncoder().encode('Test message')
const encrypted = await layer.encrypt(plaintext)
const decrypted = await layer.decrypt(encrypted)

// Verify plaintext === decrypted
assert(Array.from(decrypted).every((v, i) => v === plaintext[i]))
console.log('✅ Encryption/decryption works!')

// Test key validation
const isValid = await layer.validateKeys()
assert(isValid === true)
console.log('✅ Keys valid!')

// Test with invalid key
const invalidLayer = new ChaChaLayer({
key: new Uint8Array(16) // Wrong length!
})
const isValid2 = await invalidLayer.validateKeys()
assert(isValid2 === false)
console.log('✅ Detects invalid keys!')

console.log('All tests passed!')
}

Integration with CascadingCipherManager

// Full example with custom layers
const manager = new CascadingCipherManager()

// Mix provided and custom layers
await manager.addLayer(new CompressionLayer()) // Compress
await manager.addLayer(new ChaChaLayer({ key })) // Encrypt with ChaCha
await manager.addLayer(new DHCipherLayer(...)) // DH for keys
await manager.addLayer(new AESCipherLayer(...)) // AES backup

// Encrypt
const encrypted = await manager.encrypt('Custom layers!')

// Decrypt (handles all layers in reverse)
const decrypted = await manager.decrypt(encrypted)
console.log(decrypted) // "Custom layers!"

Advanced: Layer Metadata

interface CustomMetadata {
algorithm: string
keySize: number
timestamp: number
extraData?: any
}

// Attach metadata to your layer
export class AdvancedLayer implements CipherLayer {
name = 'Advanced Custom Layer'
private metadata: CustomMetadata

async encrypt(data: Uint8Array): Promise<Uint8Array> {
const encrypted = await this.cryptoAlgorithm(data)

// Attach metadata
const metadataBytes = new TextEncoder().encode(
JSON.stringify(this.metadata)
)

return new Uint8Array([
...metadataBytes.length, // Length prefix
...metadataBytes,
...encrypted
])
}

async decrypt(data: Uint8Array): Promise<Uint8Array> {
// Extract metadata
const metadataLen = data[0]
const metadataBytes = data.slice(1, 1 + metadataLen)
const ciphertext = data.slice(1 + metadataLen)

// Parse metadata for logging/validation
const metadata = JSON.parse(new TextDecoder().decode(metadataBytes))
console.log('Decrypted with metadata:', metadata)

return await this.cryptoDecrypt(ciphertext, metadata)
}
}

Quiz Time!

🧠 What methods must a CipherLayer implement?

Three required methods: name (property), encrypt(data), and decrypt(data). Optional: validateKeys() for key verification before encryption/decryption.

🧠 Can a layer be non-encryption (like compression)?

Yes! A layer can be any transformation—compression, encoding, or any data processing. The CascadingCipherManager treats all layers the same. Just implement encrypt and decrypt to invert each other.

🧠 Why shouldn't I create my own cryptographic primitives?

Because designing secure cryptographic algorithms is extremely hard! Even experts make mistakes. Use well-vetted libraries for the core algorithms (DH/AES/Signal/MLS/ML-KEM) and only build custom layers for composition/integration, not new primitives.

🧠 How do I integrate with my existing encryption system?

Create a CipherLayer wrapping your existing encryptor/decryptor. Your layer's encrypt() calls your legacy encrypt_v1(), and decrypt() calls your legacy decrypt_v1(). Use it in the manager like any other layer!


Can You Explain to a 5-Year-Old?

Imagine building your own lock for a toy chest:

  1. You have standard locks (like the provided ones)
  2. You can make your own custom lock with any design
  3. As long as it locks and unlocks properly, it fits in the system!
  4. You can make a fast lock, a fancy lock, or even a magical compression lock

Building your own lock follows the same rules: it must lock (encrypt) and unlock (decrypt)—how it works inside is up to you (but don't make it yourself—let the experts design the lock, you just use it)!


Key Takeaways

CipherLayer interface: Simple (name, encrypt, decrypt, validateKeys)

Custom algorithms: ChaCha20, homomorphic, compression

Legacy integration: Wrap existing encryption libraries

Mix and match: Combine custom and provided layers

Test thoroughly: Verify encryption/decryption and validation

Don't roll your own: Use vetted libraries for primitives

Document: Explain your layer's parameters and usage