UNPKG

@shogun-sdk/accounts

Version:

Shogun with Turnkey: configs, encryption, authentication with Telegram/Turnkey OIDC, etc.

133 lines (113 loc) 4.67 kB
import crypto from 'crypto'; /** * Generate a secure salt from a user ID and a private key. * @param userId The unique, immutable identifier of the user. * @returns The generated salt. * * IMPORTANT: We use the userId instead of editable fields like username for salt generation. * This is crucial for maintaining consistent encryption/decryption capabilities: * * 1. Immutability: The userId doesn't change, unlike usernames which can be edited. * If we used the username, changing it would result in a different salt, * making it impossible to decrypt previously encrypted data. * * 2. Consistency: Using the userId ensures that the salt (and thus the derived encryption key) * remains the same throughout the user's lifetime in the system. * * 3. Security: The userId is typically not known to the user, adding an extra layer of security. * * Example scenario: * - User signs up with username "ABC69" and is assigned userId "12345" * - We generate a salt and encrypt their private key using this salt * - Later, the user changes their username to "XYZ107" * - If we had used the username for salt generation, we'd no longer be able to decrypt * their private key because the new salt would be different * - By using the unchanging userId, we maintain the ability to decrypt regardless of username changes * * This approach ensures that we can always decrypt the user's encrypted data, * even if they change their editable profile information. */ export function createSecureSalt(inputString: string, privateKey: string): string { if (!privateKey) { throw new Error('privateKey is not set in the environment variables'); } // Combine the input string with the private key const combinedString = inputString + privateKey; // Create a SHA-256 hash of the combined string const hash = crypto.createHash('sha256').update(combinedString).digest('hex'); return hash; } /** * Encrypt data using AES with the derived key from the salt. * @param data The data to encrypt. * @param salt The salt to use for key derivation. * @returns The encrypted data in base64 format with IV prepended. */ export function aesEncrypt(data: string, pwd: string, salt: string): string { // Derive a key from the salt using PBKDF2 const key = crypto.pbkdf2Sync(pwd, salt, 100000, 32, 'sha256'); // Generate a random initialization vector (IV) const iv = crypto.randomBytes(16); // Create AES cipher using the key and IV const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); // Encrypt the data const encryptedBuffer = Buffer.concat([cipher.update(Buffer.from(data, 'utf8')), cipher.final()]); // Combine IV and encrypted data, then convert to base64 return Buffer.concat([iv, encryptedBuffer]).toString('base64'); } // Helper function to check if a string is base64 encoded export function isBase64(str: string): boolean { try { return Buffer.from(str, 'base64').toString('base64') === str; } catch { return false; } } export function aesDecrypt(encryptedData: string, pwdHash: string, salt: string): string { // Derive the key from the salt using PBKDF2 const key = crypto.pbkdf2Sync(pwdHash, salt, 100000, 32, 'sha256'); let iv: Buffer; let encrypted: string; // Check if the data is in the old base64 format or the new hex format if (isBase64(encryptedData)) { // Old format: base64 encoded const buffer = Buffer.from(encryptedData, 'base64'); iv = buffer.slice(0, 16); encrypted = buffer.slice(16).toString('hex'); } else { // New format: hex encoded iv = Buffer.from(encryptedData.slice(0, 32), 'hex'); encrypted = encryptedData.slice(32); } // Create AES decipher using the key and IV const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); // Decrypt the data let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } export function getSecret( id: string, encryptKey: string, publicOrganizationId: string, dataEncryptionKey: string, ): string | null { try { const pwdHash = createSecureSalt(id, publicOrganizationId); const salt = createSecureSalt(id, dataEncryptionKey); return aesDecrypt(encryptKey, pwdHash, salt); } catch (error) { console.error('Error in getSecret:', error); return null; } } export const createEncryptedValue = ( privateKey: string, id: string, publicOrganizationId: string, dataEncryptionKey: string, ) => { const pwdHash = createSecureSalt(id, publicOrganizationId); const salt = createSecureSalt(id, dataEncryptionKey); return aesEncrypt(privateKey, pwdHash, salt); };