Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 1 addition & 166 deletions apps/sim/lib/api-key/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@
*/

import { randomBytes } from 'crypto'
import {
createEncryptedApiKey,
createLegacyApiKey,
expectApiKeyInvalid,
expectApiKeyValid,
} from '@sim/testing'
import { createEncryptedApiKey, createLegacyApiKey } from '@sim/testing'
import { describe, expect, it, vi } from 'vitest'

const cryptoMock = vi.hoisted(() => ({
Expand All @@ -40,7 +35,6 @@ const cryptoMock = vi.hoisted(() => ({
vi.mock('@/lib/api-key/crypto', () => cryptoMock)

import {
authenticateApiKey,
formatApiKeyForDisplay,
getApiKeyLast4,
isEncryptedKey,
Expand Down Expand Up @@ -113,110 +107,6 @@ describe('isLegacyApiKeyFormat', () => {
})
})

describe('authenticateApiKey', () => {
describe('encrypted format key (sk-sim-) against encrypted storage', () => {
it('should authenticate matching encrypted key', async () => {
const plainKey = 'sk-sim-test-key-123'
const encryptedStorage = `mock-iv:${Buffer.from(plainKey).toString('hex')}:mock-tag`

const result = await authenticateApiKey(plainKey, encryptedStorage)
expectApiKeyValid(result)
})

it('should reject non-matching encrypted key', async () => {
const inputKey = 'sk-sim-test-key-123'
const differentKey = 'sk-sim-different-key'
const encryptedStorage = `mock-iv:${Buffer.from(differentKey).toString('hex')}:mock-tag`

const result = await authenticateApiKey(inputKey, encryptedStorage)
expectApiKeyInvalid(result)
})

it('should reject encrypted format key against plain text storage', async () => {
const inputKey = 'sk-sim-test-key-123'
const plainStorage = inputKey // Same key but stored as plain text

const result = await authenticateApiKey(inputKey, plainStorage)
expectApiKeyInvalid(result)
})
})

describe('legacy format key (sim_) against storage', () => {
it('should authenticate legacy key against encrypted storage', async () => {
const plainKey = 'sim_legacy-test-key'
const encryptedStorage = `mock-iv:${Buffer.from(plainKey).toString('hex')}:mock-tag`

const result = await authenticateApiKey(plainKey, encryptedStorage)
expectApiKeyValid(result)
})

it('should authenticate legacy key against plain text storage', async () => {
const plainKey = 'sim_legacy-test-key'
const plainStorage = plainKey

const result = await authenticateApiKey(plainKey, plainStorage)
expectApiKeyValid(result)
})

it('should reject non-matching legacy key', async () => {
const inputKey = 'sim_test-key'
const storedKey = 'sim_different-key'

const result = await authenticateApiKey(inputKey, storedKey)
expectApiKeyInvalid(result)
})
})

describe('unrecognized format keys', () => {
it('should authenticate unrecognized key against plain text match', async () => {
const plainKey = 'custom-api-key-format'
const plainStorage = plainKey

const result = await authenticateApiKey(plainKey, plainStorage)
expectApiKeyValid(result)
})

it('should authenticate unrecognized key against encrypted storage', async () => {
const plainKey = 'custom-api-key-format'
const encryptedStorage = `mock-iv:${Buffer.from(plainKey).toString('hex')}:mock-tag`

const result = await authenticateApiKey(plainKey, encryptedStorage)
expectApiKeyValid(result)
})

it('should reject non-matching unrecognized key', async () => {
const inputKey = 'custom-key-1'
const storedKey = 'custom-key-2'

const result = await authenticateApiKey(inputKey, storedKey)
expectApiKeyInvalid(result)
})
})

describe('edge cases', () => {
it('should reject empty input key', async () => {
const result = await authenticateApiKey('', 'sim_stored-key')
expectApiKeyInvalid(result)
})

it('should reject empty stored key', async () => {
const result = await authenticateApiKey('sim_input-key', '')
expectApiKeyInvalid(result)
})

it('should handle keys with special characters', async () => {
const specialKey = 'sim_key-with-special+chars/and=more'
const result = await authenticateApiKey(specialKey, specialKey)
expectApiKeyValid(result)
})

it('should be case-sensitive', async () => {
const result = await authenticateApiKey('sim_TestKey', 'sim_testkey')
expectApiKeyInvalid(result)
})
})
})

describe('isValidApiKeyFormat', () => {
it('should accept valid length keys', () => {
expect(isValidApiKeyFormat(`sim_${'a'.repeat(20)}`)).toBe(true)
Expand Down Expand Up @@ -330,58 +220,3 @@ describe('generateEncryptedApiKey', () => {
expect(key.length).toBeLessThan(100)
})
})

describe('API key lifecycle', () => {
it('should authenticate newly generated legacy key against itself (plain storage)', async () => {
const key = generateApiKey()
const result = await authenticateApiKey(key, key)
expectApiKeyValid(result)
})

it('should authenticate newly generated encrypted key against encrypted storage', async () => {
const key = generateEncryptedApiKey()
const encryptedStorage = `mock-iv:${Buffer.from(key).toString('hex')}:mock-tag`
const result = await authenticateApiKey(key, encryptedStorage)
expectApiKeyValid(result)
})

it('should reject key if storage is tampered', async () => {
const key = generateApiKey()
const lastChar = key.slice(-1)
// Ensure tampered character is different from original (handles edge case where key ends in 'X')
const tamperedChar = lastChar === 'X' ? 'Y' : 'X'
const tamperedStorage = `${key.slice(0, -1)}${tamperedChar}`
const result = await authenticateApiKey(key, tamperedStorage)
expectApiKeyInvalid(result)
})
})

describe('security considerations', () => {
it('should not accept partial key matches', async () => {
const fullKey = 'sim_abcdefghijklmnop'
const partialKey = 'sim_abcdefgh'
const result = await authenticateApiKey(partialKey, fullKey)
expectApiKeyInvalid(result)
})

it('should not accept keys with extra characters', async () => {
const storedKey = 'sim_abcdefgh'
const extendedKey = 'sim_abcdefghXXX'
const result = await authenticateApiKey(extendedKey, storedKey)
expectApiKeyInvalid(result)
})

it('should not accept key with whitespace variations', async () => {
const key = 'sim_testkey'
const keyWithSpace = ' sim_testkey'
const result = await authenticateApiKey(keyWithSpace, key)
expectApiKeyInvalid(result)
})

it('should not accept key with trailing whitespace', async () => {
const key = 'sim_testkey'
const keyWithTrailing = 'sim_testkey '
const result = await authenticateApiKey(keyWithTrailing, key)
expectApiKeyInvalid(result)
})
})
56 changes: 0 additions & 56 deletions apps/sim/lib/api-key/auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { db } from '@sim/db'
import { apiKey } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@sim/security/compare'
import { generateShortId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import {
Expand Down Expand Up @@ -32,61 +31,6 @@ export function isEncryptedKey(storedKey: string): boolean {
return storedKey.includes(':') && storedKey.split(':').length === 3
}

/**
* Authenticates an API key against a stored key, supporting both legacy and new encrypted formats
* @param inputKey - The API key provided by the client
* @param storedKey - The key stored in the database (may be plain text or encrypted)
* @returns Promise<boolean> - true if the key is valid
*/
export async function authenticateApiKey(inputKey: string, storedKey: string): Promise<boolean> {
try {
// If input key has new encrypted prefix (sk-sim-), only check against encrypted storage
if (isEncryptedApiKeyFormat(inputKey)) {
if (isEncryptedKey(storedKey)) {
try {
const { decrypted } = await decryptApiKey(storedKey)
return safeCompare(inputKey, decrypted)
} catch (decryptError) {
logger.error('Failed to decrypt stored API key:', { error: decryptError })
return false
}
}
// New format keys should never match against plain text storage
return false
}

// If input key has legacy prefix (sim_), check both encrypted and plain text
if (isLegacyApiKeyFormat(inputKey)) {
if (isEncryptedKey(storedKey)) {
try {
const { decrypted } = await decryptApiKey(storedKey)
return safeCompare(inputKey, decrypted)
} catch (decryptError) {
logger.error('Failed to decrypt stored API key:', { error: decryptError })
// Fall through to plain text comparison if decryption fails
}
}
// Legacy format can match against plain text storage
return safeCompare(inputKey, storedKey)
}

// If no recognized prefix, fall back to original behavior
if (isEncryptedKey(storedKey)) {
try {
const { decrypted } = await decryptApiKey(storedKey)
return safeCompare(inputKey, decrypted)
} catch (decryptError) {
logger.error('Failed to decrypt stored API key:', { error: decryptError })
}
}

return safeCompare(inputKey, storedKey)
} catch (error) {
logger.error('API key authentication error:', { error })
return false
}
}

/**
* Encrypts an API key for secure storage
* @param apiKey - The plain text API key to encrypt
Expand Down
Loading
Loading