Toolbox-XToolbox-X

Encrypt/Decrypt Text

A lightweight stream-cipher encryption utility using HMAC-SHA256 for keystream generation.

Cipher

A lightweight stream-cipher encryption utility using HMAC-SHA256 for keystream generation.

Overview

Cipher provides simple symmetric encryption for strings using a custom HMAC-based construction. It derives separate encryption and MAC keys from a secret and provides encryption, decryption, and validation methods. This is a pure JavaScript implementation with no external dependencies.

Info

Does not rely on Node.js or Web APIs. Works on any JS engine

Security Note

This is a custom cryptographic construction and should be used for non-critical data protection only. For production-grade security, use established libraries, Node crypto module or Web Crypto API.

Import & Setup

import { Cipher } from 'toolbox-x/hash';

// Initialize with a secret key
const cipher = new Cipher('your-secret-key-here');

Basic Usage

Encrypting/Decrypting Text

playground.ts

Validating Tokens

const isValid = cipher.isValid(encryptedToken);
// Returns: true if token is properly formatted and MAC is valid

Encryption Scheme

Key Derivation

  • Encryption Key: HMAC-SHA256(secret, "enc")
  • MAC Key: HMAC-SHA256(secret, "mac")

Encryption Process

  1. Generate random 16-byte IV from timestamp + random seed
  2. Create keystream: HMAC(encKey, iv || counter) in counter mode
  3. Ciphertext: plaintext XOR keystream
  4. Tag: HMAC(macKey, iv || ciphertext)
  5. Output: base64(iv || ciphertext || tag)

Format

[16-byte IV] + [ciphertext] + [32-byte MAC tag]

API Reference

Constructor

new Cipher(secret: string)

Creates a new Cipher instance with the specified secret.

Parameters:

  • secret: Non-empty string used for key derivation

Example:

const cipher = new Cipher(process.env.ENCRYPTION_KEY);

encrypt(text: string): string

Encrypts a UTF-8 string.

Parameters:

  • text: Plaintext string to encrypt

Returns: Base64-encoded encrypted token

Example:

const token = cipher.encrypt('Hello, World!');

decrypt(token: string): string

Decrypts a previously encrypted token.

Parameters:

  • token: Base64-encoded token from encrypt()

Returns: Decrypted plaintext string

Throws: Error if token is invalid, tampered, or MAC doesn't match

Example:

try {
  const text = cipher.decrypt(token);
} catch (error) {
  // Handle decryption failure
}

isValid(token: string): boolean

Validates token structure and MAC.

Parameters:

  • token: Base64-encoded token to validate

Returns: true if token is properly formatted and MAC is valid

Example:

if (cipher.isValid(token)) {
  // Safe to attempt decryption
}

Common Patterns

Encrypted Configuration Storage

class SecureConfig {
  private cipher: Cipher;

  constructor(secret: string) {
    this.cipher = new Cipher(secret);
  }

  storeConfig(config: object): string {
    const json = JSON.stringify(config);
    return this.cipher.encrypt(json);
  }

  loadConfig(encrypted: string): object {
    const json = this.cipher.decrypt(encrypted);
    return JSON.parse(json);
  }
}

// Usage
const secureConfig = new SecureConfig(process.env.CONFIG_SECRET);
const encrypted = secureConfig.storeConfig({ apiKey: 'abc123' });
const config = secureConfig.loadConfig(encrypted);

Secure Cookie/Storage

function createSecureStorage(cipher: Cipher) {
  return {
    setItem(key: string, value: string): void {
      const encrypted = cipher.encrypt(value);
      localStorage.setItem(key, encrypted);
    },
    
    getItem(key: string): string | null {
      const encrypted = localStorage.getItem(key);
      if (!encrypted) return null;
      
      if (!cipher.isValid(encrypted)) {
        localStorage.removeItem(key);
        return null;
      }
      
      try {
        return cipher.decrypt(encrypted);
      } catch {
        localStorage.removeItem(key);
        return null;
      }
    }
  };
}

Encrypted Messaging

playground.ts

Security Considerations

Limitations

  1. Custom Construction: Not a standard algorithm like AES-GCM
  2. Deterministic IV: IV generation uses timestamp + Math.random()
  3. JavaScript Timing: Constant-time comparisons but JavaScript may leak timing
  4. No Key Rotation: Single secret for entire lifecycle

Best Practices

// 1. Use strong, random secrets
import { randomHex, Cipher } from 'toolbox-x/hash';
const strongSecret = randomHex(64); // store it in the .env
const cipher = new Cipher(strongSecret); // use the secret from .env

// 2. Validate before decryption
function safeDecrypt(cipher: Cipher, token: string): string | null {
  if (!cipher.isValid(token)) {
    return null;
  }
  
  try {
    return cipher.decrypt(token);
  } catch {
    return null;
  }
}

// 3. Combine with other security measures
class EnhancedCipher {
  private cipher: Cipher;
  private pepper: string; // Application-wide pepper

  constructor(userSecret: string, pepper: string) {
    const combinedSecret = userSecret + pepper;
    this.cipher = new Cipher(combinedSecret);
    this.pepper = pepper;
  }
  
  // ... wrapper methods
}

When Not to Use

Do not use for:

  • Passwords (use dedicated password hashing like bcrypt)
  • Financial transactions
  • Medical records
  • Government/military data
  • Long-term sensitive data storage

Appropriate uses:

  • Temporary session data
  • Configuration values
  • Non-critical application data
  • Educational/demonstration purposes

Error Handling

Common Errors

try {
  const result = cipher.decrypt(token);
} catch (error) {
  if (error.message.includes('base64')) {
    // Invalid Base64 encoding
  } else if (error.message.includes('Malformed')) {
    // Token structure incorrect
  } else if (error.message.includes('tampered')) {
    // MAC validation failed - possible tampering
  } else {
    // Unexpected error
  }
}

Validation First Pattern

function decryptWithValidation(cipher: Cipher, token: string): {
  success: boolean;
  data?: string;
  error?: string;
} {
  if (!cipher.isValid(token)) {
    return { success: false, error: 'Invalid token' };
  }
  
  try {
    const data = cipher.decrypt(token);
    return { success: true, data };
  } catch (error) {
    return { 
      success: false, 
      error: error instanceof Error ? error.message : 'Decryption failed' 
    };
  }
}

Performance

Considerations

  • Pure JavaScript: Slower than native crypto but portable
  • HMAC-SHA256: Relatively fast for moderate data sizes
  • Memory Efficient: Uses Uint8Array for binary operations

Benchmark Example

function benchmarkCipher(cipher: Cipher, dataSize: number) {
  const data = 'x'.repeat(dataSize);
  
  console.time('encrypt');
  const encrypted = cipher.encrypt(data);
  console.timeEnd('encrypt');
  
  console.time('decrypt');
  cipher.decrypt(encrypted);
  console.timeEnd('decrypt');
  
  return encrypted.length; // Show overhead
}

Migration & Compatibility

From Simple XOR Encryption

// Before: Weak XOR encryption
function weakEncrypt(text: string, key: string): string {
  let result = '';
  for (let i = 0; i < text.length; i++) {
    result += String.fromCharCode(text.charCodeAt(i) ^ key.charCodeAt(i % key.length));
  }
  return btoa(result);
}

// After: Cipher with proper authentication
const cipher = new Cipher(key);
const strongEncrypted = cipher.encrypt(text);

Data Format Migration

If you need to migrate from an older format:

class MigratingCipher {
  private current: Cipher;
  private legacy?: Cipher;

  constructor(currentSecret: string, legacySecret?: string) {
    this.current = new Cipher(currentSecret);
    if (legacySecret) {
      this.legacy = new Cipher(legacySecret);
    }
  }

  decryptWithFallback(token: string): string {
    try {
      return this.current.decrypt(token);
    } catch (currentError) {
      if (this.legacy) {
        try {
          const result = this.legacy.decrypt(token);
          // Re-encrypt with new secret
          return this.current.encrypt(result);
        } catch (legacyError) {
          throw new Error('Decryption failed with both current and legacy secrets');
        }
      }
      throw currentError;
    }
  }
}

Examples

Encrypted API Client

class SecureApiClient {
  private cipher: Cipher;

  constructor(apiKey: string, secret: string) {
    this.cipher = new Cipher(secret);
  }

  async sendSecureRequest(endpoint: string, data: object) {
    const encryptedData = this.cipher.encrypt(JSON.stringify(data));
    
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Encrypted': 'true'
      },
      body: JSON.stringify({ data: encryptedData })
    });
    
    const result = await response.json();
    
    if (result.encrypted) {
      return JSON.parse(this.cipher.decrypt(result.data));
    }
    
    return result;
  }
}

Secure Feature Flags

class SecureFeatureManager {
  private cipher: Cipher;
  private flags: Map<string, string>;

  constructor(secret: string) {
    this.cipher = new Cipher(secret);
    this.flags = new Map();
  }

  enableFeature(userId: string, feature: string, ttl: number = 3600) {
    const expiry = Date.now() + ttl * 1000;
    const data = JSON.stringify({ feature, userId, expiry });
    const token = this.cipher.encrypt(data);
    this.flags.set(`${userId}:${feature}`, token);
  }

  isFeatureEnabled(userId: string, feature: string): boolean {
    const key = `${userId}:${feature}`;
    const token = this.flags.get(key);
    
    if (!token) return false;
    
    try {
      const data = JSON.parse(this.cipher.decrypt(token));
      if (data.expiry < Date.now()) {
        this.flags.delete(key);
        return false;
      }
      return data.userId === userId && data.feature === feature;
    } catch {
      this.flags.delete(key);
      return false;
    }
  }
}

Troubleshooting

Common Issues

Token must be a base64 string!

  • Input is not valid Base64
  • Contains non-Base64 characters
  • Encoding issues

Malformed or tampered token!

  • Token shorter than 48 bytes
  • Structure corrupted
  • Truncated during storage/transmission

Key in the token is tampered or invalid!

  • MAC validation failed
  • Wrong secret used
  • Token was modified

Debugging

function debugToken(token: string) {
  try {
    const bytes = base64ToBytes(token);
    console.log('Total length:', bytes.length);
    console.log('IV (first 16 bytes):', bytes.slice(0, 16));
    console.log('Tag (last 32 bytes):', bytes.slice(-32));
    console.log('Ciphertext length:', bytes.length - 48);
  } catch {
    console.log('Invalid Base64');
  }
}

See Also

Important

For production applications requiring strong cryptographic security, consider using the Web Crypto API or Node.js crypto module with established algorithms like AES-GCM.

Last updated: Sun, Jun 14, 2026 08:22:19PM (UTC)

On this page