mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-06 12:45:07 -05:00
- Add flatTagIndexMap for O(1) tag lookups (replaces O(n²) findIndex calls) - Memoize caret position calculation to avoid DOM manipulation on every render - Use refs for inputValue/cursorPosition to keep handleTagSelect callback stable - Change itemRefs from index-based to tag-based keys to prevent stale refs - Fix scroll jump in nested folders by removing scroll reset from registerFolder - Add onFolderEnter callback for scroll reset when entering folder via keyboard - Disable keyboard navigation wrap-around at boundaries - Simplify selection reset to single effect on flatTagList.length change Also: - Add safeCompare utility for timing-safe string comparison - Refactor webhook signature validation to use safeCompare Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
99 lines
3.0 KiB
TypeScript
99 lines
3.0 KiB
TypeScript
import { createCipheriv, createDecipheriv, randomBytes, timingSafeEqual } from 'crypto'
|
|
import { createLogger } from '@sim/logger'
|
|
import { env } from '@/lib/core/config/env'
|
|
|
|
const logger = createLogger('Encryption')
|
|
|
|
function getEncryptionKey(): Buffer {
|
|
const key = env.ENCRYPTION_KEY
|
|
if (!key || key.length !== 64) {
|
|
throw new Error('ENCRYPTION_KEY must be set to a 64-character hex string (32 bytes)')
|
|
}
|
|
return Buffer.from(key, 'hex')
|
|
}
|
|
|
|
/**
|
|
* Encrypts a secret using AES-256-GCM
|
|
* @param secret - The secret to encrypt
|
|
* @returns A promise that resolves to an object containing the encrypted secret and IV
|
|
*/
|
|
export async function encryptSecret(secret: string): Promise<{ encrypted: string; iv: string }> {
|
|
const iv = randomBytes(16)
|
|
const key = getEncryptionKey()
|
|
|
|
const cipher = createCipheriv('aes-256-gcm', key, iv)
|
|
let encrypted = cipher.update(secret, 'utf8', 'hex')
|
|
encrypted += cipher.final('hex')
|
|
|
|
const authTag = cipher.getAuthTag()
|
|
|
|
// Format: iv:encrypted:authTag
|
|
return {
|
|
encrypted: `${iv.toString('hex')}:${encrypted}:${authTag.toString('hex')}`,
|
|
iv: iv.toString('hex'),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypts an encrypted secret
|
|
* @param encryptedValue - The encrypted value in format "iv:encrypted:authTag"
|
|
* @returns A promise that resolves to an object containing the decrypted secret
|
|
*/
|
|
export async function decryptSecret(encryptedValue: string): Promise<{ decrypted: string }> {
|
|
const parts = encryptedValue.split(':')
|
|
const ivHex = parts[0]
|
|
const authTagHex = parts[parts.length - 1]
|
|
const encrypted = parts.slice(1, -1).join(':')
|
|
|
|
if (!ivHex || !encrypted || !authTagHex) {
|
|
throw new Error('Invalid encrypted value format. Expected "iv:encrypted:authTag"')
|
|
}
|
|
|
|
const key = getEncryptionKey()
|
|
const iv = Buffer.from(ivHex, 'hex')
|
|
const authTag = Buffer.from(authTagHex, 'hex')
|
|
|
|
try {
|
|
const decipher = createDecipheriv('aes-256-gcm', key, iv)
|
|
decipher.setAuthTag(authTag)
|
|
|
|
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
|
|
decrypted += decipher.final('utf8')
|
|
|
|
return { decrypted }
|
|
} catch (error: any) {
|
|
logger.error('Decryption error:', { error: error.message })
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a secure random password
|
|
* @param length - The length of the password (default: 24)
|
|
* @returns A new secure password string
|
|
*/
|
|
export function generatePassword(length = 24): string {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+='
|
|
let result = ''
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Compares two strings in constant time to prevent timing attacks.
|
|
* Used for HMAC signature validation.
|
|
* @param a - First string to compare
|
|
* @param b - Second string to compare
|
|
* @returns True if strings are equal, false otherwise
|
|
*/
|
|
export function safeCompare(a: string, b: string): boolean {
|
|
if (a.length !== b.length) {
|
|
return false
|
|
}
|
|
return timingSafeEqual(Buffer.from(a), Buffer.from(b))
|
|
}
|