Custom Layers: Building Your Own Lock
Mental Model: Creating Your Own Lock
Imagine you want a special lock for your treasure chest. You can:
- Buy a standard lock (like the provided AES/DH/Signal/MLS/ML-KEM layers)
- Build your own custom lock (implement the
CipherLayerinterface)
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:
- Performance: Use faster algorithms like ChaCha20
- Niche use cases: Homomorphic encryption for computation
- Legacy systems: Integrate with existing encryption
- Research: Test new cryptographic algorithms
- 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
- Implement all three methods (encrypt, decrypt, validateKeys)
- Test thoroughly with known plaintext/ciphertext
- Document your layer (what algorithm, what parameters)
- Handle errors gracefully (return clear error messages)
- Use established libraries (don't roll your own crypto!)
🚫 DON'T
- Create your own cryptographic primitives (no custom DH/AES!)
- Skip validation (validateKeys is essential)
- Make layers stateful (avoid storing data between encrypt/decrypt)
- Ignore error handling (return detailed errors)
- 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:
- You have standard locks (like the provided ones)
- You can make your own custom lock with any design
- As long as it locks and unlocks properly, it fits in the system!
- 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