Files
core2026/app/gateway-api/test/passwordCredential.test.ts
T
Hide_D fc7de05017 feat: 레거시 재이관과 계정 복구 기반을 추가
중앙 이전 기록 스키마와 버전 정규화기를 도입하고, 재실행 시 현행 계정 상태를 보존한다. 카카오 인증 뒤 이관 비밀번호를 1회 설정하는 흐름과 비카카오 계정용 안전한 CLI 복구 경로를 추가한다.
2026-08-17 15:55:32 +00:00

78 lines
3.6 KiB
TypeScript

import { constants, createHash, publicEncrypt } from 'node:crypto';
import { describe, expect, it } from 'vitest';
import { createInMemoryUserRepository } from '../src/auth/inMemoryUserRepository.js';
import { createPasswordEnvelopeService } from '../src/auth/passwordEnvelope.js';
import { createPasswordHasher } from '../src/auth/passwordHasher.js';
describe('password credential compatibility', () => {
it('seals browser passwords with RSA-OAEP before opening them at the gateway', () => {
const service = createPasswordEnvelopeService();
const publicKey = service.getPublicKey();
const ciphertext = publicEncrypt(
{
key: publicKey.publicKeyPem,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
},
Buffer.from('터널에-보이지-않는-비밀번호', 'utf8')
).toString('base64');
expect(ciphertext).not.toContain('비밀번호');
expect(service.open({ keyId: publicKey.keyId, ciphertext })).toBe('터널에-보이지-않는-비밀번호');
expect(() => service.open({ keyId: 'stale-key', ciphertext })).toThrow(/key has changed/i);
expect(() => service.open({ keyId: publicKey.keyId, ciphertext: '*not-base64*' })).toThrow(/base64/i);
});
it('upgrades the former core SHA-256 credential after a successful login', async () => {
const hasher = createPasswordHasher();
const users = createInMemoryUserRepository(hasher);
const user = await users.createUser({
username: 'core-legacy',
password: 'current-password',
});
user.passwordSalt = 'core-salt';
user.passwordHash = createHash('sha256').update('core-salt:current-password').digest('hex');
user.passwordResetRequired = true;
expect(await users.verifyPassword(user, 'current-password')).toBe(true);
expect(user.passwordHash.startsWith('$argon2id$')).toBe(true);
expect(user.passwordSalt).toBe('');
expect(user.passwordResetRequired).toBe(false);
});
it('upgrades an imported ref double-SHA-512 credential after a successful login', async () => {
const globalSalt = 'ref-global-salt';
const hasher = createPasswordHasher({ legacyGlobalSalt: globalSalt });
const users = createInMemoryUserRepository(hasher);
const user = await users.createUser({
username: 'ref-legacy',
password: 'current-password',
});
const userSalt = 'ref-user-salt';
const browserHash = createHash('sha512').update(`${globalSalt}current-password${globalSalt}`).digest('hex');
user.passwordSalt = userSalt;
user.passwordHash = createHash('sha512').update(`${userSalt}${browserHash}${userSalt}`).digest('hex');
user.passwordResetRequired = true;
expect(await users.verifyPassword(user, 'current-password')).toBe(true);
expect(user.passwordHash.startsWith('$argon2id$')).toBe(true);
expect(user.passwordSalt).toBe('');
expect(user.passwordResetRequired).toBe(false);
});
it('does not accept an imported ref credential without the matching global salt', async () => {
const users = createInMemoryUserRepository(createPasswordHasher());
const user = await users.createUser({
username: 'ref-no-salt',
password: 'current-password',
});
user.passwordSalt = 'ref-user-salt';
user.passwordHash = 'a'.repeat(128);
expect(await users.verifyPassword(user, 'current-password')).toBe(false);
expect(user.passwordHash).toBe('a'.repeat(128));
});
});