d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
188 lines
6.3 KiB
TypeScript
188 lines
6.3 KiB
TypeScript
import { createEnvMock } from '@sim/testing'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
vi.mock('@/lib/core/config/env', () =>
|
|
createEnvMock({
|
|
ENCRYPTION_KEY: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
|
|
})
|
|
)
|
|
|
|
import { env } from '@/lib/core/config/env'
|
|
import { decryptSecret, encryptSecret, generatePassword } from './encryption'
|
|
|
|
describe('encryptSecret', () => {
|
|
it('should encrypt a secret and return encrypted value with IV', async () => {
|
|
const secret = 'my-secret-value'
|
|
const result = await encryptSecret(secret)
|
|
|
|
expect(result.encrypted).toBeDefined()
|
|
expect(result.iv).toBeDefined()
|
|
expect(result.encrypted).toContain(':')
|
|
expect(result.iv).toHaveLength(32)
|
|
})
|
|
|
|
it('should produce different encrypted values for the same input', async () => {
|
|
const secret = 'same-secret'
|
|
const result1 = await encryptSecret(secret)
|
|
const result2 = await encryptSecret(secret)
|
|
|
|
expect(result1.encrypted).not.toBe(result2.encrypted)
|
|
expect(result1.iv).not.toBe(result2.iv)
|
|
})
|
|
|
|
it('should encrypt empty strings', async () => {
|
|
const result = await encryptSecret('')
|
|
expect(result.encrypted).toBeDefined()
|
|
expect(result.iv).toBeDefined()
|
|
})
|
|
|
|
it('should encrypt long secrets', async () => {
|
|
const longSecret = 'a'.repeat(10000)
|
|
const result = await encryptSecret(longSecret)
|
|
expect(result.encrypted).toBeDefined()
|
|
})
|
|
|
|
it('should encrypt secrets with special characters', async () => {
|
|
const specialSecret = '!@#$%^&*()_+-=[]{}|;\':",.<>?/`~\n\t\r'
|
|
const result = await encryptSecret(specialSecret)
|
|
expect(result.encrypted).toBeDefined()
|
|
})
|
|
|
|
it('should encrypt unicode characters', async () => {
|
|
const unicodeSecret = 'Hello !"#$%&\'()*+,-./0123456789:;<=>?@'
|
|
const result = await encryptSecret(unicodeSecret)
|
|
expect(result.encrypted).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('decryptSecret', () => {
|
|
it('should decrypt an encrypted secret back to original value', async () => {
|
|
const originalSecret = 'my-secret-value'
|
|
const { encrypted } = await encryptSecret(originalSecret)
|
|
const { decrypted } = await decryptSecret(encrypted)
|
|
|
|
expect(decrypted).toBe(originalSecret)
|
|
})
|
|
|
|
it('should decrypt very short secrets', async () => {
|
|
const { encrypted } = await encryptSecret('a')
|
|
const { decrypted } = await decryptSecret(encrypted)
|
|
expect(decrypted).toBe('a')
|
|
})
|
|
|
|
it('should decrypt long secrets', async () => {
|
|
const longSecret = 'b'.repeat(10000)
|
|
const { encrypted } = await encryptSecret(longSecret)
|
|
const { decrypted } = await decryptSecret(encrypted)
|
|
expect(decrypted).toBe(longSecret)
|
|
})
|
|
|
|
it('should decrypt secrets with special characters', async () => {
|
|
const specialSecret = '!@#$%^&*()_+-=[]{}|;\':",.<>?/`~\n\t\r'
|
|
const { encrypted } = await encryptSecret(specialSecret)
|
|
const { decrypted } = await decryptSecret(encrypted)
|
|
expect(decrypted).toBe(specialSecret)
|
|
})
|
|
|
|
it('should throw error for invalid encrypted format (missing parts)', async () => {
|
|
await expect(decryptSecret('invalid')).rejects.toThrow(
|
|
'Invalid encrypted value format. Expected "iv:encrypted:authTag"'
|
|
)
|
|
})
|
|
|
|
it('should throw error for invalid encrypted format (only two parts)', async () => {
|
|
await expect(decryptSecret('part1:part2')).rejects.toThrow(
|
|
'Invalid encrypted value format. Expected "iv:encrypted:authTag"'
|
|
)
|
|
})
|
|
|
|
it('should throw error for tampered ciphertext', async () => {
|
|
const { encrypted } = await encryptSecret('original-secret')
|
|
const parts = encrypted.split(':')
|
|
parts[1] = `tampered${parts[1].slice(8)}`
|
|
const tamperedEncrypted = parts.join(':')
|
|
|
|
await expect(decryptSecret(tamperedEncrypted)).rejects.toThrow()
|
|
})
|
|
|
|
it('should throw error for tampered auth tag', async () => {
|
|
const { encrypted } = await encryptSecret('original-secret')
|
|
const parts = encrypted.split(':')
|
|
parts[2] = '00000000000000000000000000000000'
|
|
const tamperedEncrypted = parts.join(':')
|
|
|
|
await expect(decryptSecret(tamperedEncrypted)).rejects.toThrow()
|
|
})
|
|
|
|
it('should throw error for invalid IV', async () => {
|
|
const { encrypted } = await encryptSecret('original-secret')
|
|
const parts = encrypted.split(':')
|
|
parts[0] = '00000000000000000000000000000000'
|
|
const tamperedEncrypted = parts.join(':')
|
|
|
|
await expect(decryptSecret(tamperedEncrypted)).rejects.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('generatePassword', () => {
|
|
it('should generate password with default length of 24', () => {
|
|
const password = generatePassword()
|
|
expect(password).toHaveLength(24)
|
|
})
|
|
|
|
it('should generate password with custom length', () => {
|
|
const password = generatePassword(32)
|
|
expect(password).toHaveLength(32)
|
|
})
|
|
|
|
it('should generate password with minimum length', () => {
|
|
const password = generatePassword(1)
|
|
expect(password).toHaveLength(1)
|
|
})
|
|
|
|
it('should generate different passwords on each call', () => {
|
|
const passwords = new Set()
|
|
for (let i = 0; i < 100; i++) {
|
|
passwords.add(generatePassword())
|
|
}
|
|
expect(passwords.size).toBeGreaterThan(90)
|
|
})
|
|
|
|
it('should only contain allowed characters', () => {
|
|
const allowedChars =
|
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+='
|
|
const password = generatePassword(1000)
|
|
|
|
for (const char of password) {
|
|
expect(allowedChars).toContain(char)
|
|
}
|
|
})
|
|
|
|
it('should handle zero length', () => {
|
|
const password = generatePassword(0)
|
|
expect(password).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('encryption key validation', () => {
|
|
const originalEncryptionKey = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
|
|
|
afterEach(() => {
|
|
;(env as Record<string, string>).ENCRYPTION_KEY = originalEncryptionKey
|
|
})
|
|
|
|
it('should throw error when ENCRYPTION_KEY is not set', async () => {
|
|
;(env as Record<string, string>).ENCRYPTION_KEY = ''
|
|
await expect(encryptSecret('test')).rejects.toThrow(
|
|
'ENCRYPTION_KEY must be set to a 64-character hex string (32 bytes)'
|
|
)
|
|
})
|
|
|
|
it('should throw error when ENCRYPTION_KEY is wrong length', async () => {
|
|
;(env as Record<string, string>).ENCRYPTION_KEY = '0123456789abcdef'
|
|
await expect(encryptSecret('test')).rejects.toThrow(
|
|
'ENCRYPTION_KEY must be set to a 64-character hex string (32 bytes)'
|
|
)
|
|
})
|
|
})
|