🔐 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
288 lines
8.4 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|