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
Validating Tokens
const isValid = cipher.isValid(encryptedToken);
// Returns: true if token is properly formatted and MAC is validEncryption Scheme
Key Derivation
- Encryption Key:
HMAC-SHA256(secret, "enc") - MAC Key:
HMAC-SHA256(secret, "mac")
Encryption Process
- Generate random 16-byte IV from timestamp + random seed
- Create keystream:
HMAC(encKey, iv || counter)in counter mode - Ciphertext:
plaintext XOR keystream - Tag:
HMAC(macKey, iv || ciphertext) - 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 fromencrypt()
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
Security Considerations
Limitations
- Custom Construction: Not a standard algorithm like
AES-GCM - Deterministic IV: IV generation uses timestamp +
Math.random() - JavaScript Timing: Constant-time comparisons but JavaScript may leak timing
- 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
Uint8Arrayfor 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
- Signet - For authentication tokens
- Encoding Utilities - For
Base64,UTF-8and binary conversions
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)
