Skip to content

Commit bb330d9

Browse files
committed
chore: refactor hasher
1 parent 16ea00b commit bb330d9

8 files changed

Lines changed: 27 additions & 29 deletions

src/engine/core/admin-api-key-utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,3 @@ function hexToBytes(hex: string): Uint8Array {
101101
}
102102
return out;
103103
}
104-

src/engine/core/sdk-key-service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {LRUCache} from 'lru-cache';
22
import type {SdkKeyInfo} from '../engine';
3-
import type {HashingService} from './hashing-service';
43
import type {ReplicaService} from './replica';
54
import {extractSdkKeyId} from './sdk-key-utils';
5+
import type {SecureHashingService} from './secure-hashing-service';
66
import type {Service} from './service';
77

88
export class SdkKeyService implements Service {
@@ -16,7 +16,7 @@ export class SdkKeyService implements Service {
1616

1717
constructor(
1818
private readonly replicaService: ReplicaService,
19-
private readonly hasher: HashingService,
19+
private readonly hasher: SecureHashingService,
2020
) {}
2121
async start(): Promise<void> {}
2222

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
// Use Web Crypto API (available in modern browsers and Node.js >= 18)
1+
import {timingSafeEqual} from 'node:crypto';
22

3-
export interface HashingService {
3+
export interface SecureHashingService {
44
hash(source: string): Promise<string>;
55
verify(hash: string, source: string): Promise<boolean>;
66
}
77

88
// Simple SHA-256 hashing service (unsalted, deterministic).
99
// NOTE: This is weaker than Argon2 (no memory hardness). Consider reintroducing
1010
// a stronger KDF if tokens need resistance against offline brute-force.
11-
export function createSha256HashingService(): HashingService {
11+
export function createSha256HashingService(): SecureHashingService {
1212
function toHex(buffer: ArrayBuffer): string {
1313
const bytes = new Uint8Array(buffer);
1414
let hex = '';
@@ -29,7 +29,13 @@ export function createSha256HashingService(): HashingService {
2929
return sha256(token);
3030
},
3131
async verify(hash: string, token: string) {
32-
return (await sha256(token)) === hash;
32+
return timingSafeEqualString(await sha256(token), hash);
3333
},
3434
};
3535
}
36+
37+
export function timingSafeEqualString(a: string, b: string): boolean {
38+
const aBuffer = Buffer.from(a);
39+
const bBuffer = Buffer.from(b);
40+
return aBuffer.length === bBuffer.length && timingSafeEqual(aBuffer, bBuffer);
41+
}

src/engine/core/use-cases/create-sdk-key-use-case.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {BadRequestError} from '../errors';
2-
import type {HashingService} from '../hashing-service';
32
import {getUserIdFromIdentity, type Identity} from '../identity';
43
import {buildRawSdkKey, getSdkKeyPrefix, getSdkKeySuffix} from '../sdk-key-utils';
4+
import type {SecureHashingService} from '../secure-hashing-service';
55
import {createAuditLogId} from '../stores/audit-log-store';
66
import type {TransactionalUseCase} from '../use-case';
77
import {createUuidV7} from '../uuid';
@@ -25,7 +25,7 @@ export interface CreateSdkKeyResponse {
2525
}
2626

2727
export function createCreateSdkKeyUseCase(deps: {
28-
hasher: HashingService;
28+
secureHasher: SecureHashingService;
2929
}): TransactionalUseCase<CreateSdkKeyRequest, CreateSdkKeyResponse> {
3030
return async (ctx, tx, req) => {
3131
await tx.permissionService.ensureCanManageSdkKeys(ctx, {
@@ -44,7 +44,7 @@ export function createCreateSdkKeyUseCase(deps: {
4444
const sdkKeyId = createUuidV7();
4545
// Embed apiTokenId into token for future extraction
4646
const sdkKey = buildRawSdkKey(sdkKeyId);
47-
const sdkKeyHash = await deps.hasher.hash(sdkKey);
47+
const sdkKeyHash = await deps.secureHasher.hash(sdkKey);
4848
const keyPrefix = getSdkKeyPrefix(sdkKey);
4949
const keySuffix = getSdkKeySuffix(sdkKey);
5050
const now = new Date();

src/engine/core/use-cases/verify-admin-api-key-use-case.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import {getSuperuserApiKey} from '@/environment';
2-
import {timingSafeEqual} from 'crypto';
32
import type {Kysely} from 'kysely';
43
import {LRUCache} from 'lru-cache';
5-
import {extractAdminApiKeyId, hashAdminApiKey} from '../admin-api-key-utils';
4+
import {extractAdminApiKeyId} from '../admin-api-key-utils';
65
import type {AdminApiKeyScope, DB} from '../db';
76
import {
87
createApiKeyIdentity,
98
createSuperuserIdentity,
109
type ApiKeyIdentity,
1110
type SuperuserIdentity,
1211
} from '../identity';
12+
import {timingSafeEqualString, type SecureHashingService} from '../secure-hashing-service';
1313
import type {UseCase} from '../use-case';
1414

1515
export interface VerifyAdminApiKeyRequest {
@@ -26,6 +26,7 @@ export type VerifyAdminApiKeyResponse =
2626

2727
export interface VerifyAdminApiKeyUseCaseDeps {
2828
db: Kysely<DB>;
29+
secureHasher: SecureHashingService;
2930
}
3031

3132
export function createVerifyAdminApiKeyUseCase(
@@ -45,12 +46,7 @@ export function createVerifyAdminApiKeyUseCase(
4546
// Check if this is the superuser API key using constant-time comparison
4647
const superuserKey = getSuperuserApiKey();
4748
if (superuserKey) {
48-
const keyBuffer = Buffer.from(key);
49-
const superuserKeyBuffer = Buffer.from(superuserKey);
50-
if (
51-
superuserKeyBuffer.length === keyBuffer.length &&
52-
timingSafeEqual(keyBuffer, superuserKeyBuffer)
53-
) {
49+
if (timingSafeEqualString(superuserKey, key)) {
5450
return {status: 'valid', identity: createSuperuserIdentity()};
5551
}
5652
}
@@ -65,9 +61,6 @@ export function createVerifyAdminApiKeyUseCase(
6561
return {status: 'invalid', reason: 'invalid_format'};
6662
}
6763

68-
// Hash the key for verification
69-
const keyHash = await hashAdminApiKey(key);
70-
7164
// Fetch the key by hash
7265
const adminKey = await db
7366
.selectFrom('admin_api_keys')
@@ -79,7 +72,7 @@ export function createVerifyAdminApiKeyUseCase(
7972
return {status: 'invalid', reason: 'invalid_key'};
8073
}
8174

82-
if (adminKey.key_hash !== keyHash) {
75+
if (!(await deps.secureHasher.verify(adminKey.key_hash, key))) {
8376
return {status: 'invalid', reason: 'invalid_key'};
8477
}
8578

src/engine/core/use-cases/verify-sdk-key-use-case.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {LRUCache} from 'lru-cache';
22
import type {SdkKeyInfo} from '../../engine';
3-
import type {HashingService} from '../hashing-service';
43
import type {ReplicaService} from '../replica';
54
import {extractSdkKeyId} from '../sdk-key-utils';
5+
import type {SecureHashingService} from '../secure-hashing-service';
66
import type {UseCase} from '../use-case';
77

88
export interface VerifySdkKeyRequest {
@@ -13,7 +13,7 @@ export type VerifySdkKeyResponse = SdkKeyInfo | null;
1313

1414
export interface VerifySdkKeyUseCaseDeps {
1515
replicaService: ReplicaService;
16-
hasher: HashingService;
16+
hasher: SecureHashingService;
1717
}
1818

1919
export function createVerifySdkKeyUseCase(

src/engine/edge.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import path from 'node:path';
44
import {GLOBAL_CONTEXT} from './core/context';
55
import {type DateProvider, DefaultDateProvider} from './core/date-provider';
66
import {EventHub} from './core/event-hub';
7-
import {createSha256HashingService} from './core/hashing-service';
87
import {createLogger, type LogLevel} from './core/logger';
98
import {prepareDb} from './core/prepare-db';
109
import {ReplicaService} from './core/replica';
1110
import {ReplicaEventBus} from './core/replica-event-bus';
11+
import {createSha256HashingService} from './core/secure-hashing-service';
1212
import type {Service} from './core/service';
1313
import {ReplicaStore} from './core/stores/replica-store';
1414
import {createGetProjectEventsUseCase} from './core/use-cases/get-project-events-use-case';

src/engine/engine.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import {type DateProvider, DefaultDateProvider} from './core/date-provider';
99
import type {DB} from './core/db';
1010
import {type EmailService, PreferencesAwareEmailService} from './core/email-service';
1111
import {EventHubPublisher} from './core/event-hub';
12-
import {createSha256HashingService} from './core/hashing-service';
1312
import {createLogger, type Logger, type LogLevel} from './core/logger';
1413
import {PermissionService} from './core/permission-service';
1514
import {prepareDb} from './core/prepare-db';
1615
import {ProjectQueryService} from './core/project-query-service';
1716
import {ProposalService} from './core/proposal-service';
1817
import {type AppHubEvents} from './core/replica';
18+
import {createSha256HashingService} from './core/secure-hashing-service';
1919
import {AdminApiKeyStore} from './core/stores/admin-api-key-store';
2020
import {AuditLogStore} from './core/stores/audit-log-store';
2121
import {ConfigProposalStore} from './core/stores/config-proposal-store';
@@ -272,7 +272,7 @@ export async function createEngine(options: EngineOptions) {
272272

273273
const dateProvider = options.dateProvider ?? new DefaultDateProvider();
274274

275-
const hasher = createSha256HashingService();
275+
const secureHasher = createSha256HashingService();
276276

277277
const transactionalUseCases = {
278278
getConfigList: createGetConfigListUseCase({}),
@@ -313,7 +313,7 @@ export async function createEngine(options: EngineOptions) {
313313
updateProjectEnvironmentsOrder: createUpdateProjectEnvironmentsOrderUseCase({dateProvider}),
314314
deleteProjectEnvironment: createDeleteProjectEnvironmentUseCase({dateProvider}),
315315
restoreConfigVersion: createRestoreConfigVersionUseCase(),
316-
createSdkKey: createCreateSdkKeyUseCase({hasher: hasher}),
316+
createSdkKey: createCreateSdkKeyUseCase({secureHasher}),
317317
// Combined use cases for page data
318318
getConfigPageData: createGetConfigPageDataUseCase(),
319319
getNewConfigPageData: createGetNewConfigPageDataUseCase(),
@@ -374,7 +374,7 @@ export async function createEngine(options: EngineOptions) {
374374
useCases: {
375375
...engineUseCases,
376376
getHealth: createGetHealthUseCase(),
377-
verifyAdminApiKey: createVerifyAdminApiKeyUseCase({db}),
377+
verifyAdminApiKey: createVerifyAdminApiKeyUseCase({db, secureHasher}),
378378
},
379379
mail: options.emailService,
380380
stores: {

0 commit comments

Comments
 (0)