ihm_client/src/services/__tests__/secure-credentials.service.test.ts
NicolasCantu bf680ab6dd ci: docker_tag=pbkdf2-credentials
🔐 Implémentation PBKDF2 avec credentials navigateur

 Fonctionnalités ajoutées:
- SecureCredentialsService avec PBKDF2 (100k itérations)
- Chiffrement AES-GCM des clés spend/scan
- Interface utilisateur complète pour gestion credentials
- Tests unitaires complets
- Architecture modulaire avec EventBus
- Gestion mémoire optimisée
- Performance monitoring
- Web Workers pour encodage asynchrone

🛡️ Sécurité:
- Dérivation PBKDF2 avec salt unique
- Chiffrement AES-GCM des clés sensibles
- Validation force mot de passe
- Stockage sécurisé IndexedDB + WebAuthn
- Logging sécurisé sans exposition données

🔧 Corrections:
- Erreur 500 résolue (clé dupliquée package.json)
- Configuration Vite simplifiée
- Dépendances manquantes corrigées

📊 Améliorations:
- Architecture découplée avec repositories
- Services spécialisés (PairingService, etc.)
- Monitoring performance et mémoire
- Tests avec couverture complète
- Documentation technique détaillée
2025-10-23 12:51:49 +02:00

288 lines
8.4 KiB
TypeScript

/**
* Tests unitaires pour SecureCredentialsService
*/
import { SecureCredentialsService, CredentialData } from '../secure-credentials.service';
// Mock des APIs du navigateur
const mockCrypto = {
getRandomValues: jest.fn((arr) => {
for (let i = 0; i < arr.length; i++) {
arr[i] = Math.floor(Math.random() * 256);
}
return arr;
}),
subtle: {
importKey: jest.fn(),
deriveKey: jest.fn(),
deriveBits: jest.fn(),
encrypt: jest.fn(),
decrypt: jest.fn()
}
};
Object.defineProperty(window, 'crypto', { value: mockCrypto });
// Mock d'IndexedDB
const mockIndexedDB = {
open: jest.fn()
};
Object.defineProperty(window, 'indexedDB', { value: mockIndexedDB });
// Mock de navigator.credentials
const mockCredentials = {
create: jest.fn()
};
Object.defineProperty(navigator, 'credentials', { value: mockCredentials });
describe('SecureCredentialsService', () => {
let service: SecureCredentialsService;
beforeEach(() => {
service = SecureCredentialsService.getInstance();
jest.clearAllMocks();
});
describe('generateSecureCredentials', () => {
it('should generate secure credentials with PBKDF2', async () => {
const mockMasterKey = new CryptoKey();
const mockSpendKey = 'spend-key-123';
const mockScanKey = 'scan-key-456';
mockCrypto.subtle.importKey.mockResolvedValue(mockMasterKey);
mockCrypto.subtle.deriveKey.mockResolvedValue(mockMasterKey);
mockCrypto.subtle.deriveBits
.mockResolvedValueOnce(new ArrayBuffer(32)) // spend key
.mockResolvedValueOnce(new ArrayBuffer(32)); // scan key
const credentials = await service.generateSecureCredentials('test-password');
expect(credentials).toBeDefined();
expect(credentials.spendKey).toBeDefined();
expect(credentials.scanKey).toBeDefined();
expect(credentials.salt).toBeDefined();
expect(credentials.iterations).toBe(100000);
expect(credentials.timestamp).toBeGreaterThan(0);
});
it('should use custom options when provided', async () => {
const options = {
iterations: 50000,
saltLength: 16,
keyLength: 16
};
const credentials = await service.generateSecureCredentials('test-password', options);
expect(credentials.iterations).toBe(50000);
expect(credentials.salt.length).toBe(16);
});
});
describe('validatePasswordStrength', () => {
it('should validate strong password', () => {
const result = service.validatePasswordStrength('StrongPass123!');
expect(result.isValid).toBe(true);
expect(result.score).toBe(5);
expect(result.feedback).toHaveLength(0);
});
it('should validate weak password', () => {
const result = service.validatePasswordStrength('weak');
expect(result.isValid).toBe(false);
expect(result.score).toBeLessThan(4);
expect(result.feedback.length).toBeGreaterThan(0);
});
it('should provide specific feedback for password issues', () => {
const result = service.validatePasswordStrength('lowercase');
expect(result.feedback).toContain('Le mot de passe doit contenir au moins une majuscule');
expect(result.feedback).toContain('Le mot de passe doit contenir au moins un chiffre');
});
});
describe('storeCredentials', () => {
it('should store credentials securely', async () => {
const mockCredential = { id: 'credential-id' };
const mockMasterKey = new CryptoKey();
const mockEncryptedData = new Uint8Array(32);
mockCredentials.create.mockResolvedValue(mockCredential);
mockCrypto.subtle.importKey.mockResolvedValue(mockMasterKey);
mockCrypto.subtle.deriveKey.mockResolvedValue(mockMasterKey);
mockCrypto.subtle.encrypt.mockResolvedValue(mockEncryptedData);
// Mock IndexedDB
const mockDB = {
transaction: jest.fn().mockReturnValue({
objectStore: jest.fn().mockReturnValue({
put: jest.fn().mockReturnValue({
onsuccess: jest.fn(),
onerror: jest.fn()
})
})
})
};
mockIndexedDB.open.mockReturnValue({
onsuccess: jest.fn(),
onupgradeneeded: jest.fn(),
result: mockDB
});
const credentialData: CredentialData = {
spendKey: 'spend-key',
scanKey: 'scan-key',
salt: new Uint8Array(32),
iterations: 100000,
timestamp: Date.now()
};
await expect(service.storeCredentials(credentialData, 'password')).resolves.not.toThrow();
});
});
describe('retrieveCredentials', () => {
it('should retrieve and decrypt credentials', async () => {
const mockMasterKey = new CryptoKey();
const mockDecryptedData = new TextEncoder().encode('decrypted-key');
// Mock IndexedDB retrieval
const mockDB = {
transaction: jest.fn().mockReturnValue({
objectStore: jest.fn().mockReturnValue({
get: jest.fn().mockReturnValue({
onsuccess: jest.fn(),
onerror: jest.fn(),
result: {
encryptedSpendKey: new Uint8Array(32),
encryptedScanKey: new Uint8Array(32),
salt: new Uint8Array(32),
iterations: 100000,
timestamp: Date.now()
}
})
})
})
};
mockIndexedDB.open.mockReturnValue({
onsuccess: jest.fn(),
onupgradeneeded: jest.fn(),
result: mockDB
});
mockCrypto.subtle.importKey.mockResolvedValue(mockMasterKey);
mockCrypto.subtle.deriveKey.mockResolvedValue(mockMasterKey);
mockCrypto.subtle.decrypt.mockResolvedValue(mockDecryptedData);
const credentials = await service.retrieveCredentials('password');
expect(credentials).toBeDefined();
expect(credentials?.spendKey).toBeDefined();
expect(credentials?.scanKey).toBeDefined();
});
it('should return null when no credentials exist', async () => {
const mockDB = {
transaction: jest.fn().mockReturnValue({
objectStore: jest.fn().mockReturnValue({
get: jest.fn().mockReturnValue({
onsuccess: jest.fn(),
onerror: jest.fn(),
result: null
})
})
})
};
mockIndexedDB.open.mockReturnValue({
onsuccess: jest.fn(),
onupgradeneeded: jest.fn(),
result: mockDB
});
const credentials = await service.retrieveCredentials('password');
expect(credentials).toBeNull();
});
});
describe('hasCredentials', () => {
it('should return true when credentials exist', async () => {
const mockDB = {
transaction: jest.fn().mockReturnValue({
objectStore: jest.fn().mockReturnValue({
get: jest.fn().mockReturnValue({
onsuccess: jest.fn(),
onerror: jest.fn(),
result: { exists: true }
})
})
})
};
mockIndexedDB.open.mockReturnValue({
onsuccess: jest.fn(),
onupgradeneeded: jest.fn(),
result: mockDB
});
const hasCredentials = await service.hasCredentials();
expect(hasCredentials).toBe(true);
});
it('should return false when no credentials exist', async () => {
const mockDB = {
transaction: jest.fn().mockReturnValue({
objectStore: jest.fn().mockReturnValue({
get: jest.fn().mockReturnValue({
onsuccess: jest.fn(),
onerror: jest.fn(),
result: null
})
})
})
};
mockIndexedDB.open.mockReturnValue({
onsuccess: jest.fn(),
onupgradeneeded: jest.fn(),
result: mockDB
});
const hasCredentials = await service.hasCredentials();
expect(hasCredentials).toBe(false);
});
});
describe('deleteCredentials', () => {
it('should delete credentials successfully', async () => {
const mockDB = {
transaction: jest.fn().mockReturnValue({
objectStore: jest.fn().mockReturnValue({
delete: jest.fn().mockReturnValue({
onsuccess: jest.fn(),
onerror: jest.fn()
})
})
})
};
mockIndexedDB.open.mockReturnValue({
onsuccess: jest.fn(),
onupgradeneeded: jest.fn(),
result: mockDB
});
await expect(service.deleteCredentials()).resolves.not.toThrow();
});
});
});