/**
* FILE: lib/encryption.cjs
* STATUS: ACTIVE
* RUNTIME: Bare
* MODULE: CommonJS
*
* PURPOSE:
* Password-based encryption for mnemonics at rest
* Uses PBKDF2 for key derivation and AES-256-GCM for encryption
*
* USED BY:
* - wallet-server.cjs (mnemonic encryption/decryption)
* - hyper-wallet-advanced-cli.cjs (mnemonic encryption/decryption)
*
* KEY EXPORTS:
* - encrypt(plaintext, password) - Encrypt with password
* - decrypt(encrypted, password) - Decrypt with password
*
* SECURITY:
* - PBKDF2-SHA256 for key derivation (100k iterations, bare-crypto compatible)
* - AES-256-GCM authenticated encryption
* - Random salt and IV per encryption
* - Auth tag prevents tampering
*
* NOTE: Using PBKDF2 instead of scrypt due to bare-crypto limitations
*/
const crypto = require('bare-crypto')
const b4a = require('b4a')
// PBKDF2 parameters (bare-crypto compatible)
const PBKDF2_ITERATIONS = 100000 // NIST recommended minimum
const PBKDF2_KEYLEN = 32 // 256-bit key
const PBKDF2_DIGEST = 'sha256' // Hash algorithm
// AES-GCM parameters
const IV_LENGTH = 12 // 96-bit IV (recommended for GCM)
const SALT_LENGTH = 32 // 256-bit salt
const TAG_LENGTH = 16 // 128-bit auth tag
/**
* Derive encryption key from password using PBKDF2
* (bare-crypto compatible - scrypt not available)
*
* @param {string} password - User password
* @param {Buffer} salt - Random salt
* @returns {Buffer} - 256-bit derived key
*/
function deriveKey(password, salt) {
// Derive key using PBKDF2
// Note: This is CPU-intensive (intentional security feature)
const key = crypto.pbkdf2Sync(
password,
salt,
PBKDF2_ITERATIONS,
PBKDF2_KEYLEN,
PBKDF2_DIGEST
)
return key
}
/**
* Encrypt plaintext with password
*
* @param {string} plaintext - Text to encrypt (e.g., mnemonic)
* @param {string} password - Encryption password
* @returns {string} - Base64-encoded encrypted data with salt and IV
*
* Format: base64(salt + iv + ciphertext + tag)
*/
function encrypt(plaintext, password) {
// Generate random salt and IV
const salt = crypto.randomBytes(SALT_LENGTH)
const iv = crypto.randomBytes(IV_LENGTH)
// Derive encryption key
const key = deriveKey(password, salt)
// Create cipher
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
// Encrypt plaintext
const plaintextBuffer = b4a.from(plaintext, 'utf8')
const encrypted = b4a.concat([
cipher.update(plaintextBuffer),
cipher.final()
])
// Get authentication tag
const tag = cipher.getAuthTag()
// Concatenate: salt + iv + ciphertext + tag
const result = b4a.concat([salt, iv, encrypted, tag])
// Return as base64
return b4a.toString(result, 'base64')
}
/**
* Decrypt encrypted data with password
*
* @param {string} encrypted - Base64-encoded encrypted data
* @param {string} password - Decryption password
* @returns {string} - Decrypted plaintext
* @throws {Error} - If password is wrong or data is corrupted
*/
function decrypt(encrypted, password) {
// Decode from base64
const data = b4a.from(encrypted, 'base64')
// Extract components
const salt = data.slice(0, SALT_LENGTH)
const iv = data.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH)
const ciphertext = data.slice(SALT_LENGTH + IV_LENGTH, data.length - TAG_LENGTH)
const tag = data.slice(data.length - TAG_LENGTH)
// Derive decryption key
const key = deriveKey(password, salt)
// Create decipher
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
decipher.setAuthTag(tag)
try {
// Decrypt ciphertext
const decrypted = b4a.concat([
decipher.update(ciphertext),
decipher.final()
])
return b4a.toString(decrypted, 'utf8')
} catch (err) {
// Auth tag verification failed or wrong password
throw new Error('Decryption failed - wrong password or corrupted data')
}
}
/**
* Check if encrypted data format is valid
*
* @param {string} encrypted - Base64-encoded encrypted data
* @returns {boolean} - True if format is valid
*/
function isValidFormat(encrypted) {
try {
const data = b4a.from(encrypted, 'base64')
const minLength = SALT_LENGTH + IV_LENGTH + TAG_LENGTH
return data.length >= minLength
} catch {
return false
}
}
module.exports = {
encrypt,
decrypt,
isValidFormat,
// Export constants for testing
PBKDF2_ITERATIONS,
PBKDF2_KEYLEN,
PBKDF2_DIGEST,
IV_LENGTH,
SALT_LENGTH,
TAG_LENGTH
}