ci: docker_tag=dev-test

**Motivations :**
- Implémentation du système de sélection de mode de sécurité
- Séparation claire entre les données de sécurité et les données du wallet
- Suppression des duplications entre 'encrypted-pbkdf2-key' et 'pbkdf2-key'
- Architecture modulaire pour la gestion des credentials

**Modifications :**
- Ajout du composant security-mode-selector pour la sélection du mode de sécurité
- Création des pages séquentielles : security-setup, wallet-setup, birthday-setup
- Implémentation des services de credentials (encryption, storage, webauthn)
- Ajout du service security-mode pour la gestion des modes de sécurité
- Correction du stockage des clés PBKDF2 avec le securityMode dynamique
- Suppression des méthodes redondantes dans StorageService
- Nettoyage des appels redondants dans secure-credentials.service.ts

**Pages affectées :**
- src/components/security-mode-selector/ (nouveau composant)
- src/pages/security-setup/ (nouvelle page)
- src/pages/wallet-setup/ (nouvelle page)
- src/pages/birthday-setup/ (nouvelle page)
- src/services/credentials/ (nouveaux services)
- src/services/security-mode.service.ts (nouveau service)
- src/services/secure-credentials.service.ts (modifié)
- src/services/database.service.ts (modifié)
- src/router.ts (modifié)
- src/pages/home/home.ts (modifié)
This commit is contained in:
NicolasCantu 2025-10-24 18:29:31 +02:00
parent c385f23e8f
commit 653c7f32ca
22 changed files with 4680 additions and 687 deletions

View File

@ -4,7 +4,7 @@ server {
# Redirection des requêtes HTTP vers Vite # Redirection des requêtes HTTP vers Vite
location / { location / {
proxy_pass http://localhost:3003; proxy_pass http://localhost:3004;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade"; proxy_set_header Connection "Upgrade";
@ -45,4 +45,4 @@ server {
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,X-Requested-With" always; add_header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,X-Requested-With" always;
} }
} }

View File

@ -0,0 +1,297 @@
.security-mode-selector {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.security-mode-header {
text-align: center;
margin-bottom: 30px;
}
.security-mode-header h2 {
color: #2c3e50;
margin-bottom: 10px;
}
.security-mode-header p {
color: #7f8c8d;
font-size: 16px;
}
.security-options {
display: grid;
gap: 20px;
margin-bottom: 30px;
}
.security-option {
border: 2px solid #e1e8ed;
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
background: #fff;
}
.security-option:hover {
border-color: #3498db;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.15);
transform: translateY(-2px);
}
.security-option.selected {
border-color: #27ae60;
background: #f8fff8;
box-shadow: 0 4px 12px rgba(39, 174, 96, 0.15);
}
.option-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.option-icon {
font-size: 24px;
margin-right: 12px;
}
.option-title {
font-size: 18px;
font-weight: 600;
color: #2c3e50;
flex: 1;
}
.security-level {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.security-level.high {
background: #d4edda;
color: #155724;
}
.security-level.medium {
background: #fff3cd;
color: #856404;
}
.security-level.low {
background: #f8d7da;
color: #721c24;
}
.security-level.critical {
background: #f5c6cb;
color: #721c24;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
.option-description {
color: #5a6c7d;
margin-bottom: 12px;
line-height: 1.5;
}
.option-features {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.feature {
background: #e8f5e8;
color: #2d5a2d;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.option-warnings {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.warning {
background: #ffeaa7;
color: #d63031;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.warning.critical {
background: #fab1a0;
color: #d63031;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.5; }
}
.security-actions {
display: flex;
justify-content: center;
gap: 15px;
padding-top: 20px;
border-top: 1px solid #e1e8ed;
}
.btn-primary, .btn-secondary, .btn-danger {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: #27ae60;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #229954;
transform: translateY(-1px);
}
.btn-primary:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.btn-secondary {
background: #95a5a6;
color: white;
}
.btn-secondary:hover {
background: #7f8c8d;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #c0392b;
}
.btn-danger:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
/* Modal styles */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
}
.modal-content {
background: white;
border-radius: 12px;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #e1e8ed;
}
.modal-header h3 {
margin: 0;
color: #e74c3c;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #e1e8ed;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.warning-actions {
margin-top: 20px;
padding: 15px;
background: #fff3cd;
border-radius: 8px;
border-left: 4px solid #ffc107;
}
.warning-actions label {
display: flex;
align-items: center;
cursor: pointer;
font-weight: 500;
}
.warning-actions input[type="checkbox"] {
margin-right: 10px;
transform: scale(1.2);
}
/* Responsive */
@media (max-width: 768px) {
.security-mode-selector {
padding: 15px;
}
.security-options {
gap: 15px;
}
.security-option {
padding: 15px;
}
.option-header {
flex-direction: column;
align-items: flex-start;
}
.option-title {
margin: 8px 0;
}
.security-actions {
flex-direction: column;
}
}

View File

@ -0,0 +1,130 @@
<div id="security-mode-selector" class="security-mode-selector">
<div class="security-mode-header">
<h2>🔐 Mode de Sécurisation</h2>
<p>Choisissez comment vous souhaitez sécuriser vos clés privées :</p>
</div>
<div class="security-options">
<!-- Proton Pass -->
<div class="security-option" data-mode="proton-pass">
<div class="option-header">
<div class="option-icon">🔒</div>
<div class="option-title">Proton Pass</div>
<div class="security-level high">Sécurisé</div>
</div>
<div class="option-description">
Utilise Proton Pass pour l'authentification biométrique et la gestion des clés
</div>
<div class="option-features">
<span class="feature">✅ Authentification biométrique</span>
<span class="feature">✅ Chiffrement end-to-end</span>
<span class="feature">✅ Synchronisation sécurisée</span>
</div>
</div>
<!-- OS Authenticator -->
<div class="security-option" data-mode="os">
<div class="option-header">
<div class="option-icon">🖥️</div>
<div class="option-title">Authentificateur OS</div>
<div class="security-level high">Sécurisé</div>
</div>
<div class="option-description">
Utilise l'authentificateur intégré de votre système d'exploitation
</div>
<div class="option-features">
<span class="feature">✅ Windows Hello / Touch ID / Face ID</span>
<span class="feature">✅ Chiffrement matériel</span>
<span class="feature">✅ Protection par mot de passe</span>
</div>
</div>
<!-- Navigateur -->
<div class="security-option" data-mode="browser">
<div class="option-header">
<div class="option-icon">🌐</div>
<div class="option-title">Navigateur</div>
<div class="security-level medium">Moyennement sécurisé</div>
</div>
<div class="option-description">
Utilise les fonctionnalités de sécurité du navigateur
</div>
<div class="option-features">
<span class="feature">✅ WebAuthn standard</span>
<span class="feature">⚠️ Dépendant du navigateur</span>
<span class="feature">⚠️ Moins sécurisé que les options OS</span>
</div>
</div>
<!-- Application 2FA -->
<div class="security-option" data-mode="2fa">
<div class="option-header">
<div class="option-icon">📱</div>
<div class="option-title">Application 2FA</div>
<div class="security-level low">⚠️ Non sécurisé</div>
</div>
<div class="option-description">
Stockage en clair avec authentification par application 2FA
</div>
<div class="option-warnings">
<span class="warning">⚠️ Clés stockées en clair</span>
<span class="warning">⚠️ Risque de compromission</span>
<span class="warning">⚠️ Non recommandé pour des données sensibles</span>
</div>
</div>
<!-- Aucune sécurité -->
<div class="security-option" data-mode="none">
<div class="option-header">
<div class="option-icon">🚨</div>
<div class="option-title">Aucune Sécurité</div>
<div class="security-level critical">DANGEREUX</div>
</div>
<div class="option-description">
Stockage en clair sans aucune protection
</div>
<div class="option-warnings">
<span class="warning critical">🚨 Clés stockées en clair</span>
<span class="warning critical">🚨 Accès non protégé</span>
<span class="warning critical">🚨 RISQUE ÉLEVÉ</span>
</div>
</div>
</div>
<div class="security-actions">
<button id="confirm-security-mode" class="btn-primary" disabled>
Confirmer le Mode de Sécurisation
</button>
<button id="cancel-security-mode" class="btn-secondary">
Annuler
</button>
</div>
<!-- Modal de confirmation pour les modes non sécurisés -->
<div id="security-warning-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>⚠️ Attention - Mode de Sécurisation Non Recommandé</h3>
</div>
<div class="modal-body">
<div id="warning-content">
<!-- Contenu généré dynamiquement -->
</div>
<div class="warning-actions">
<label>
<input type="checkbox" id="understand-risks">
Je comprends les risques et souhaite continuer
</label>
</div>
</div>
<div class="modal-footer">
<button id="confirm-risky-mode" class="btn-danger" disabled>
Continuer Malgré les Risques
</button>
<button id="cancel-risky-mode" class="btn-secondary">
Choisir un Autre Mode
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,382 @@
/**
* SecurityModeSelector - Composant de sélection du mode de sécurisation
* Permet à l'utilisateur de choisir comment sécuriser ses clés privées
*/
export type SecurityMode = 'proton-pass' | 'os' | 'browser' | '2fa' | 'none';
export interface SecurityModeConfig {
mode: SecurityMode;
name: string;
description: string;
securityLevel: 'high' | 'medium' | 'low' | 'critical';
requiresConfirmation: boolean;
warnings: string[];
}
export class SecurityModeSelector {
private container: HTMLElement;
private selectedMode: SecurityMode | null = null;
private onModeSelected: (mode: SecurityMode) => void;
private onCancel: () => void;
constructor(
container: HTMLElement,
onModeSelected: (mode: SecurityMode) => void,
onCancel: () => void
) {
this.container = container;
this.onModeSelected = onModeSelected;
this.onCancel = onCancel;
this.init();
}
private init(): void {
this.render();
this.attachEventListeners();
}
private render(): void {
this.container.innerHTML = `
<div class="security-mode-selector">
<div class="security-mode-header">
<h2>🔐 Mode de Sécurisation</h2>
<p>Choisissez comment vous souhaitez sécuriser vos clés privées :</p>
</div>
<div class="security-options">
${this.getSecurityOptionsHTML()}
</div>
<div class="security-actions">
<button id="confirm-security-mode" class="btn-primary" disabled>
Confirmer le Mode de Sécurisation
</button>
<button id="cancel-security-mode" class="btn-secondary">
Annuler
</button>
</div>
${this.getWarningModalHTML()}
</div>
`;
}
private getSecurityOptionsHTML(): string {
const options = this.getSecurityModes();
return options.map(option => `
<div class="security-option" data-mode="${option.mode}">
<div class="option-header">
<div class="option-icon">${this.getModeIcon(option.mode)}</div>
<div class="option-title">${option.name}</div>
<div class="security-level ${option.securityLevel}">
${this.getSecurityLevelText(option.securityLevel)}
</div>
</div>
<div class="option-description">${option.description}</div>
${this.getModeFeaturesHTML(option)}
</div>
`).join('');
}
private getModeFeaturesHTML(option: SecurityModeConfig): string {
if (option.securityLevel === 'low' || option.securityLevel === 'critical') {
return `
<div class="option-warnings">
${option.warnings.map(warning => `
<span class="warning ${option.securityLevel === 'critical' ? 'critical' : ''}">
${warning}
</span>
`).join('')}
</div>
`;
} else {
return `
<div class="option-features">
${this.getModeFeatures(option.mode).map(feature => `
<span class="feature">${feature}</span>
`).join('')}
</div>
`;
}
}
private getWarningModalHTML(): string {
return `
<div id="security-warning-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3> Attention - Mode de Sécurisation Non Recommandé</h3>
</div>
<div class="modal-body">
<div id="warning-content">
<!-- Contenu généré dynamiquement -->
</div>
<div class="warning-actions">
<label>
<input type="checkbox" id="understand-risks">
Je comprends les risques et souhaite continuer
</label>
</div>
</div>
<div class="modal-footer">
<button id="confirm-risky-mode" class="btn-danger" disabled>
Continuer Malgré les Risques
</button>
<button id="cancel-risky-mode" class="btn-secondary">
Choisir un Autre Mode
</button>
</div>
</div>
</div>
`;
}
private getSecurityModes(): SecurityModeConfig[] {
return [
{
mode: 'proton-pass',
name: 'Proton Pass',
description: 'Utilise Proton Pass pour l\'authentification biométrique et la gestion des clés',
securityLevel: 'high',
requiresConfirmation: false,
warnings: []
},
{
mode: 'os',
name: 'Authentificateur OS',
description: 'Utilise l\'authentificateur intégré de votre système d\'exploitation',
securityLevel: 'high',
requiresConfirmation: false,
warnings: []
},
{
mode: 'browser',
name: 'Navigateur',
description: 'Utilise les fonctionnalités de sécurité du navigateur',
securityLevel: 'medium',
requiresConfirmation: false,
warnings: []
},
{
mode: '2fa',
name: 'Application 2FA',
description: 'Stockage en clair avec authentification par application 2FA',
securityLevel: 'low',
requiresConfirmation: true,
warnings: [
'⚠️ Clés stockées en clair',
'⚠️ Risque de compromission',
'⚠️ Non recommandé pour des données sensibles'
]
},
{
mode: 'none',
name: 'Aucune Sécurité',
description: 'Stockage en clair sans aucune protection',
securityLevel: 'critical',
requiresConfirmation: true,
warnings: [
'🚨 Clés stockées en clair',
'🚨 Accès non protégé',
'🚨 RISQUE ÉLEVÉ'
]
}
];
}
private getModeIcon(mode: SecurityMode): string {
const icons = {
'proton-pass': '🔒',
'os': '🖥️',
'browser': '🌐',
'2fa': '📱',
'none': '🚨'
};
return icons[mode];
}
private getSecurityLevelText(level: string): string {
const texts = {
'high': 'Sécurisé',
'medium': 'Moyennement sécurisé',
'low': '⚠️ Non sécurisé',
'critical': 'DANGEREUX'
};
return texts[level as keyof typeof texts];
}
private getModeFeatures(mode: SecurityMode): string[] {
const features = {
'proton-pass': [
'✅ Authentification biométrique',
'✅ Chiffrement end-to-end',
'✅ Synchronisation sécurisée'
],
'os': [
'✅ Windows Hello / Touch ID / Face ID',
'✅ Chiffrement matériel',
'✅ Protection par mot de passe'
],
'browser': [
'✅ WebAuthn standard',
'⚠️ Dépendant du navigateur',
'⚠️ Moins sécurisé que les options OS'
],
'2fa': [],
'none': []
};
return features[mode];
}
private attachEventListeners(): void {
// Sélection d'un mode
this.container.addEventListener('click', (e) => {
const option = (e.target as HTMLElement).closest('.security-option');
if (option) {
this.selectMode(option.dataset.mode as SecurityMode);
}
});
// Confirmation du mode
this.container.addEventListener('click', (e) => {
if ((e.target as HTMLElement).id === 'confirm-security-mode') {
this.confirmSelection();
}
});
// Annulation
this.container.addEventListener('click', (e) => {
if ((e.target as HTMLElement).id === 'cancel-security-mode') {
this.onCancel();
}
});
// Gestion de la modal d'avertissement
this.attachWarningModalListeners();
}
private attachWarningModalListeners(): void {
// Checkbox de compréhension des risques
this.container.addEventListener('change', (e) => {
if ((e.target as HTMLElement).id === 'understand-risks') {
const checkbox = e.target as HTMLInputElement;
const confirmBtn = this.container.querySelector('#confirm-risky-mode') as HTMLButtonElement;
if (confirmBtn) {
confirmBtn.disabled = !checkbox.checked;
}
}
});
// Confirmation du mode risqué
this.container.addEventListener('click', (e) => {
if ((e.target as HTMLElement).id === 'confirm-risky-mode') {
this.hideWarningModal();
this.onModeSelected(this.selectedMode!);
}
});
// Annulation du mode risqué
this.container.addEventListener('click', (e) => {
if ((e.target as HTMLElement).id === 'cancel-risky-mode') {
this.hideWarningModal();
this.clearSelection();
}
});
}
private selectMode(mode: SecurityMode): void {
// Désélectionner tous les modes
this.container.querySelectorAll('.security-option').forEach(option => {
option.classList.remove('selected');
});
// Sélectionner le nouveau mode
const selectedOption = this.container.querySelector(`[data-mode="${mode}"]`);
if (selectedOption) {
selectedOption.classList.add('selected');
this.selectedMode = mode;
this.updateConfirmButton();
}
}
private updateConfirmButton(): void {
const confirmBtn = this.container.querySelector('#confirm-security-mode') as HTMLButtonElement;
if (confirmBtn) {
confirmBtn.disabled = !this.selectedMode;
}
}
private confirmSelection(): void {
if (!this.selectedMode) return;
const modeConfig = this.getSecurityModes().find(m => m.mode === this.selectedMode);
if (modeConfig?.requiresConfirmation) {
this.showWarningModal(modeConfig);
} else {
this.onModeSelected(this.selectedMode);
}
}
private showWarningModal(modeConfig: SecurityModeConfig): void {
const modal = this.container.querySelector('#security-warning-modal') as HTMLElement;
const warningContent = this.container.querySelector('#warning-content') as HTMLElement;
if (modal && warningContent) {
warningContent.innerHTML = `
<div style="margin-bottom: 20px;">
<h4>Vous avez choisi : <strong>${modeConfig.name}</strong></h4>
<p>${modeConfig.description}</p>
</div>
<div style="background: #f8d7da; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545;">
<h5 style="color: #721c24; margin-top: 0;"> Risques identifiés :</h5>
<ul style="color: #721c24; margin-bottom: 0;">
${modeConfig.warnings.map(warning => `<li>${warning}</li>`).join('')}
</ul>
</div>
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin-top: 15px;">
<p style="color: #856404; margin: 0;">
<strong>Recommandation :</strong>
${modeConfig.securityLevel === 'low'
? 'Nous vous recommandons fortement de choisir un mode plus sécurisé comme Proton Pass ou l\'authentificateur OS.'
: 'Ce mode présente des risques de sécurité élevés. Assurez-vous de comprendre les implications.'
}
</p>
</div>
`;
modal.style.display = 'flex';
}
}
private hideWarningModal(): void {
const modal = this.container.querySelector('#security-warning-modal') as HTMLElement;
if (modal) {
modal.style.display = 'none';
}
}
private clearSelection(): void {
this.selectedMode = null;
this.container.querySelectorAll('.security-option').forEach(option => {
option.classList.remove('selected');
});
this.updateConfirmButton();
}
public show(): void {
this.container.style.display = 'block';
}
public hide(): void {
this.container.style.display = 'none';
}
public destroy(): void {
this.container.innerHTML = '';
}
}

View File

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Configuration de la Date Anniversaire - LeCoffre</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
max-width: 500px;
width: 100%;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 30px;
}
.status {
text-align: center;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.status.loading {
background: #e3f2fd;
color: #1976d2;
}
.status.success {
background: #e8f5e8;
color: #2e7d32;
}
.status.error {
background: #ffebee;
color: #c62828;
}
.progress {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin: 20px 0;
}
.progress-bar {
height: 100%;
background: #667eea;
width: 0%;
transition: width 0.3s ease;
}
.continue-btn {
width: 100%;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
padding: 15px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
margin-top: 20px;
transition: background 0.3s ease;
}
.continue-btn:hover {
background: #5a6fd8;
}
.continue-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="container">
<h1>🎂 Configuration de la Date Anniversaire</h1>
<p class="subtitle">Mise à jour de la date anniversaire et scan des blocs</p>
<div class="status loading" id="status">
🔄 Connexion aux relais...
</div>
<div class="progress">
<div class="progress-bar" id="progressBar"></div>
</div>
<button class="continue-btn" id="continueBtn" disabled>Terminer l'initialisation</button>
</div>
<script type="module" src="./birthday-setup.ts"></script>
</body>
</html>

View File

@ -0,0 +1,120 @@
/**
* Page de configuration de la date anniversaire
* Mise à jour de la date anniversaire et scan des blocs
*/
document.addEventListener('DOMContentLoaded', async () => {
console.log('🎂 Birthday setup page loaded');
const status = document.getElementById('status') as HTMLDivElement;
const progressBar = document.getElementById('progressBar') as HTMLDivElement;
const continueBtn = document.getElementById('continueBtn') as HTMLButtonElement;
try {
// Étape 1: Connexion aux relais
updateStatus('🌐 Connexion aux relais...', 'loading');
updateProgress(20);
try {
console.log('🔄 Importing services...');
const serviceModule = await import('../../services/service');
console.log('✅ Service module imported:', Object.keys(serviceModule));
// La classe Services est exportée par défaut
const Services = serviceModule.default;
if (!Services) {
throw new Error('Services class not found in default export');
}
console.log('🔄 Waiting for services to be ready...');
// Attendre que les services soient initialisés
let attempts = 0;
const maxAttempts = 30;
const delayMs = 2000;
let services;
while (attempts < maxAttempts) {
try {
console.log(`🔄 Attempting to get services (attempt ${attempts + 1}/${maxAttempts})...`);
services = await Services.getInstance();
console.log('✅ Services initialized successfully');
break;
} catch (error) {
console.log(`⏳ Services not ready yet (attempt ${attempts + 1}/${maxAttempts}):`, error.message);
attempts++;
if (attempts >= maxAttempts) {
throw new Error(`Services failed to initialize after ${maxAttempts} attempts.`);
}
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
// Connexion aux relais
await services.connectAllRelays();
console.log('✅ Relays connected successfully');
// Étape 2: Mise à jour de la date anniversaire
updateStatus('🎂 Mise à jour de la date anniversaire...', 'loading');
updateProgress(40);
// Attendre que les relais soient prêts
await services.getRelayReadyPromise();
console.log('✅ Communication handshake completed');
// Mettre à jour la date anniversaire du wallet
await services.updateDeviceBlockHeight();
console.log('✅ Birthday updated successfully');
// Étape 3: Scan des blocs
updateStatus('🔍 Scan des blocs en cours...', 'loading');
updateProgress(60);
// Effectuer le scan initial des blocs
await services.ensureCompleteInitialScan();
console.log('✅ Initial block scan completed');
// Étape 4: Synchronisation des processus
updateStatus('🔄 Synchronisation des processus...', 'loading');
updateProgress(80);
// Restaurer les processus depuis la base de données
await services.restoreProcessesFromDB();
console.log('✅ Process synchronization completed');
} catch (error) {
console.error('❌ Services not available:', error);
updateStatus('❌ Erreur: Services non disponibles', 'error');
throw error;
}
// Étape 5: Finalisation
updateStatus('✅ Configuration terminée avec succès!', 'success');
updateProgress(100);
// Activer le bouton continuer
continueBtn.disabled = false;
console.log('🎉 Birthday setup completed successfully');
} catch (error) {
console.error('❌ Error during birthday setup:', error);
updateStatus('❌ Erreur lors de la configuration de la date anniversaire', 'error');
}
// Gestion du bouton continuer
continueBtn.addEventListener('click', () => {
console.log('🏠 Redirecting to main application...');
// Rediriger vers l'application principale
window.location.href = '/';
});
function updateStatus(message: string, type: 'loading' | 'success' | 'error') {
status.textContent = message;
status.className = `status ${type}`;
}
function updateProgress(percent: number) {
progressBar.style.width = `${percent}%`;
}
});

View File

@ -38,8 +38,10 @@ export async function initHomePage(): Promise<void> {
// Set up iframe pairing button listeners // Set up iframe pairing button listeners
setupIframePairingButtons(); setupIframePairingButtons();
// Set up main pairing interface // Set up main pairing interface (avec protection contre les appels multiples)
setupMainPairing(); if (!isMainPairingSetup) {
setupMainPairing();
}
// Set up account actions // Set up account actions
setupAccountActions(); setupAccountActions();
@ -63,6 +65,20 @@ export async function initHomePage(): Promise<void> {
console.log('🔧 Getting services instance...'); console.log('🔧 Getting services instance...');
const service = await Services.getInstance(); const service = await Services.getInstance();
// D'abord vérifier la sécurité avant de créer le wallet
console.log('🔐 Checking security configuration...');
const { SecureCredentialsService } = await import('../../services/secure-credentials.service');
const secureCredentialsService = SecureCredentialsService.getInstance();
const hasCredentials = await secureCredentialsService.hasCredentials();
if (!hasCredentials) {
console.log('🔐 No security credentials found, user must configure security first...');
// Afficher le sélecteur de mode de sécurité
await handleMainPairing();
return;
}
// Check if wallet exists, create if not // Check if wallet exists, create if not
console.log('🔍 Checking for existing wallet...'); console.log('🔍 Checking for existing wallet...');
const existingDevice = await service.getDeviceFromDatabase(); const existingDevice = await service.getDeviceFromDatabase();
@ -87,17 +103,32 @@ export async function initHomePage(): Promise<void> {
}); });
} }
// Trigger WebAuthn authentication first // Trigger WebAuthn authentication
console.log('🔐 Triggering WebAuthn authentication...'); console.log('🔐 Triggering WebAuthn authentication...');
await handleMainPairing(); await handleMainPairing();
// Attendre que les credentials soient réellement disponibles avant de continuer
console.log('⏳ Waiting for credentials to be fully available...');
await waitForCredentialsAvailability();
console.log('✅ Credentials confirmed as available, proceeding...');
// After WebAuthn, get device address and setup UI // After WebAuthn, get device address and setup UI
console.log('🔧 Getting device address...'); console.log('🔧 Getting device address...');
const spAddress = await service.getDeviceAddress();
console.log('🔧 Generating create button...'); try {
generateCreateBtn(); const spAddress = await service.getDeviceAddress();
console.log('🔧 Displaying emojis...'); console.log('🔧 Generating create button...');
displayEmojis(spAddress); generateCreateBtn();
console.log('🔧 Displaying emojis...');
displayEmojis(spAddress);
} catch (error) {
console.error('❌ Failed to get device address:', error);
if (error.message.includes('Wallet keys not available')) {
console.error('❌ Wallet keys not available - authentication failed');
throw new Error('Authentication failed - wallet keys not available');
}
throw error;
}
console.log('✅ Home page initialization completed'); console.log('✅ Home page initialization completed');
} catch (error) { } catch (error) {
@ -403,8 +434,19 @@ export function setupIframePairingButtons() {
} }
} }
// Variable pour éviter les appels multiples à setupMainPairing
let isMainPairingSetup = false;
// Main Pairing Interface - Automatic WebAuthn trigger // Main Pairing Interface - Automatic WebAuthn trigger
export function setupMainPairing(): void { export function setupMainPairing(): void {
// Protection contre les appels multiples
if (isMainPairingSetup) {
console.log('🔐 Main pairing already setup, skipping...');
return;
}
isMainPairingSetup = true;
const container = getCorrectDOM('login-4nk-component') as HTMLElement; const container = getCorrectDOM('login-4nk-component') as HTMLElement;
const mainStatus = container.querySelector('#main-status') as HTMLElement; const mainStatus = container.querySelector('#main-status') as HTMLElement;
@ -435,72 +477,504 @@ function setupUserInteractionListener(): void {
console.log('🔐 User interaction listeners set up'); console.log('🔐 User interaction listeners set up');
} }
/**
* Affiche le sélecteur de mode de sécurisation
*/
async function showSecurityModeSelector(): Promise<void> {
return new Promise((resolve) => {
// Créer le conteneur pour le sélecteur
const selectorContainer = document.createElement('div');
selectorContainer.id = 'security-mode-selector-container';
selectorContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
backdrop-filter: blur(5px);
`;
// Créer le contenu du sélecteur
const selectorContent = document.createElement('div');
selectorContent.style.cssText = `
background: white;
border-radius: 12px;
padding: 30px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
`;
selectorContent.innerHTML = `
<div style="text-align: center; margin-bottom: 30px;">
<h2 style="color: #2c3e50; margin-bottom: 10px;">🔐 Mode de Sécurisation</h2>
<p style="color: #7f8c8d;">Choisissez comment vous souhaitez sécuriser vos clés privées :</p>
</div>
<div style="display: grid; gap: 15px; margin-bottom: 30px;">
<div class="security-option" data-mode="proton-pass" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 20px; margin-right: 10px;">🔒</span>
<span style="font-weight: 600; color: #2c3e50;">Proton Pass</span>
<span style="background: #d4edda; color: #155724; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">Sécurisé</span>
</div>
<div style="color: #5a6c7d; font-size: 14px;">Utilise Proton Pass pour l'authentification biométrique</div>
</div>
<div class="security-option" data-mode="os" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 20px; margin-right: 10px;">🖥</span>
<span style="font-weight: 600; color: #2c3e50;">Authentificateur OS</span>
<span style="background: #d4edda; color: #155724; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">Sécurisé</span>
</div>
<div style="color: #5a6c7d; font-size: 14px;">Utilise l'authentificateur intégré de votre système</div>
</div>
<div class="security-option" data-mode="browser" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 20px; margin-right: 10px;">🌐</span>
<span style="font-weight: 600; color: #2c3e50;">Navigateur</span>
<span style="background: #fff3cd; color: #856404; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">Moyennement sécurisé</span>
</div>
<div style="color: #5a6c7d; font-size: 14px;">Utilise les fonctionnalités de sécurité du navigateur</div>
</div>
<div class="security-option" data-mode="2fa" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 20px; margin-right: 10px;">📱</span>
<span style="font-weight: 600; color: #2c3e50;">Application 2FA</span>
<span style="background: #f8d7da; color: #721c24; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;"> Non sécurisé</span>
</div>
<div style="color: #5a6c7d; font-size: 14px;">Stockage en clair avec authentification 2FA</div>
</div>
<div class="security-option" data-mode="password" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 20px; margin-right: 10px;">🔑</span>
<span style="font-weight: 600; color: #2c3e50;">Mot de Passe</span>
<span style="background: #fff3cd; color: #856404; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;"> Non sauvegardé</span>
</div>
<div style="color: #5a6c7d; font-size: 14px;">Chiffrement par mot de passe (non sauvegardé, non récupérable)</div>
</div>
<div class="security-option" data-mode="none" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 20px; margin-right: 10px;">🚨</span>
<span style="font-weight: 600; color: #2c3e50;">Aucune Sécurité</span>
<span style="background: #f5c6cb; color: #721c24; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">DANGEREUX</span>
</div>
<div style="color: #5a6c7d; font-size: 14px;">Stockage en clair sans aucune protection</div>
</div>
</div>
<div style="text-align: center;">
<button id="confirm-security-mode" style="background: #27ae60; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px; opacity: 0.5;" disabled>
Confirmer le Mode de Sécurisation
</button>
<button id="cancel-security-mode" style="background: #95a5a6; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px;">
Annuler
</button>
</div>
`;
selectorContainer.appendChild(selectorContent);
document.body.appendChild(selectorContainer);
let selectedMode: string | null = null;
// Gestion des événements
const options = selectorContent.querySelectorAll('.security-option');
const confirmBtn = selectorContent.querySelector('#confirm-security-mode') as HTMLButtonElement;
const cancelBtn = selectorContent.querySelector('#cancel-security-mode') as HTMLButtonElement;
// Sélection d'un mode
options.forEach(option => {
option.addEventListener('click', () => {
options.forEach(opt => opt.style.borderColor = '#e1e8ed');
option.style.borderColor = '#27ae60';
option.style.background = '#f8fff8';
selectedMode = option.getAttribute('data-mode');
confirmBtn.disabled = false;
confirmBtn.style.opacity = '1';
});
// Effet hover
option.addEventListener('mouseenter', () => {
if (option.style.borderColor !== '#27ae60') {
option.style.borderColor = '#3498db';
}
});
option.addEventListener('mouseleave', () => {
if (option.style.borderColor !== '#27ae60') {
option.style.borderColor = '#e1e8ed';
}
});
});
// Confirmation
confirmBtn.addEventListener('click', async () => {
if (selectedMode) {
console.log(`🔐 Security mode selected: ${selectedMode}`);
// Vérifier si le mode nécessite une confirmation
const { SecurityModeService } = await import('../../services/security-mode.service');
const securityModeService = SecurityModeService.getInstance();
const modeConfig = securityModeService.getSecurityModeConfig(selectedMode as any);
if (modeConfig.requiresConfirmation) {
// Afficher une alerte de sécurité
const confirmed = await showSecurityWarning(modeConfig);
if (!confirmed) {
return; // L'utilisateur a annulé
}
}
// Définir le mode de sécurisation
await securityModeService.setSecurityMode(selectedMode as any);
// Fermer le sélecteur
document.body.removeChild(selectorContainer);
// Réinitialiser les flags pour permettre la relance
isPairingInProgress = false;
pairingAttempts = 0;
// Relancer l'authentification avec le mode sélectionné
await handleMainPairing();
resolve();
}
});
// Annulation
cancelBtn.addEventListener('click', () => {
console.log('❌ Security mode selection cancelled');
document.body.removeChild(selectorContainer);
resolve();
});
// Fermer avec Escape
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
document.body.removeChild(selectorContainer);
document.removeEventListener('keydown', handleEscape);
resolve();
}
};
document.addEventListener('keydown', handleEscape);
});
}
/**
* Affiche une alerte de sécurité pour les modes non recommandés
*/
async function showSecurityWarning(modeConfig: any): Promise<boolean> {
return new Promise((resolve) => {
const warningContainer = document.createElement('div');
warningContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 10001;
backdrop-filter: blur(5px);
`;
const warningContent = document.createElement('div');
warningContent.style.cssText = `
background: white;
border-radius: 12px;
padding: 30px;
max-width: 500px;
width: 90%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
`;
warningContent.innerHTML = `
<div style="text-align: center; margin-bottom: 20px;">
<h3 style="color: #e74c3c; margin-bottom: 10px;"> Attention - Mode de Sécurisation Non Recommandé</h3>
</div>
<div style="margin-bottom: 20px;">
<h4>Vous avez choisi : <strong>${modeConfig.name}</strong></h4>
<p>${modeConfig.description}</p>
</div>
<div style="background: #f8d7da; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545; margin-bottom: 20px;">
<h5 style="color: #721c24; margin-top: 0;"> Risques identifiés :</h5>
<ul style="color: #721c24; margin-bottom: 0;">
${modeConfig.warnings.map((warning: string) => `<li>${warning}</li>`).join('')}
</ul>
</div>
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin-bottom: 20px;">
<p style="color: #856404; margin: 0;">
<strong>Recommandation :</strong>
${modeConfig.securityLevel === 'low'
? 'Nous vous recommandons fortement de choisir un mode plus sécurisé comme Proton Pass ou l\'authentificateur OS.'
: 'Ce mode présente des risques de sécurité élevés. Assurez-vous de comprendre les implications.'
}
</p>
</div>
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<label style="display: flex; align-items: center; cursor: pointer; font-weight: 500;">
<input type="checkbox" id="understand-risks" style="margin-right: 10px; transform: scale(1.2);">
Je comprends les risques et souhaite continuer
</label>
</div>
<div style="text-align: center;">
<button id="confirm-risky-mode" style="background: #e74c3c; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px; opacity: 0.5;" disabled>
Continuer Malgré les Risques
</button>
<button id="cancel-risky-mode" style="background: #95a5a6; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px;">
Choisir un Autre Mode
</button>
</div>
`;
warningContainer.appendChild(warningContent);
document.body.appendChild(warningContainer);
const understandCheckbox = warningContent.querySelector('#understand-risks') as HTMLInputElement;
const confirmBtn = warningContent.querySelector('#confirm-risky-mode') as HTMLButtonElement;
const cancelBtn = warningContent.querySelector('#cancel-risky-mode') as HTMLButtonElement;
// Gestion de la checkbox
understandCheckbox.addEventListener('change', () => {
confirmBtn.disabled = !understandCheckbox.checked;
confirmBtn.style.opacity = understandCheckbox.checked ? '1' : '0.5';
});
// Confirmation
confirmBtn.addEventListener('click', () => {
document.body.removeChild(warningContainer);
resolve(true);
});
// Annulation
cancelBtn.addEventListener('click', () => {
document.body.removeChild(warningContainer);
resolve(false);
});
// Fermer avec Escape
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
document.body.removeChild(warningContainer);
document.removeEventListener('keydown', handleEscape);
resolve(false);
}
};
document.addEventListener('keydown', handleEscape);
});
}
/**
* Attend que les credentials soient réellement disponibles
*/
// Variable pour éviter les appels multiples à waitForCredentialsAvailability
let isWaitingForCredentials = false;
async function waitForCredentialsAvailability(): Promise<void> {
// Protection contre les appels multiples
if (isWaitingForCredentials) {
console.log('🔍 Already waiting for credentials, skipping...');
return;
}
isWaitingForCredentials = true;
try {
const { secureCredentialsService } = await import('../../services/secure-credentials.service');
let attempts = 0;
const maxAttempts = 20;
const delayMs = 1000;
while (attempts < maxAttempts) {
attempts++;
console.log(`🔍 Vérification de la disponibilité des credentials (tentative ${attempts}/${maxAttempts})...`);
try {
// Vérifier que les credentials sont réellement disponibles et accessibles
const credentials = await secureCredentialsService.retrieveCredentials('');
if (credentials && credentials.spendKey && credentials.scanKey) {
console.log('✅ Credentials confirmés comme disponibles');
return;
} else {
throw new Error('Credentials incomplets');
}
} catch (error) {
console.warn(`⚠️ Credentials pas encore disponibles (tentative ${attempts}):`, error);
if (attempts < maxAttempts) {
console.log(`⏳ Attente de ${delayMs}ms avant la prochaine tentative...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
throw new Error('Credentials non disponibles après toutes les tentatives');
} finally {
isWaitingForCredentials = false;
}
}
// Variables pour éviter les appels multiples
let isPairingInProgress = false;
let pairingAttempts = 0;
const MAX_PAIRING_ATTEMPTS = 1;
async function handleMainPairing(): Promise<void> { async function handleMainPairing(): Promise<void> {
// Protection renforcée contre les appels multiples
if (isPairingInProgress) {
console.log('🔐 Pairing already in progress, skipping...');
return;
}
if (pairingAttempts >= MAX_PAIRING_ATTEMPTS) {
console.log('🔐 Maximum pairing attempts reached, skipping...');
return;
}
isPairingInProgress = true;
pairingAttempts++;
const container = getCorrectDOM('login-4nk-component') as HTMLElement; const container = getCorrectDOM('login-4nk-component') as HTMLElement;
const mainStatus = container.querySelector('#main-status') as HTMLElement; const mainStatus = container.querySelector('#main-status') as HTMLElement;
try { try {
// Update UI to show authentication in progress // Vérifier si un mode de sécurisation est déjà sélectionné
if (mainStatus) { const { SecurityModeService } = await import('../../services/security-mode.service');
mainStatus.innerHTML = '<div class="spinner"></div><span>Authenticating with browser...</span>'; const securityModeService = SecurityModeService.getInstance();
let currentMode: string | null = null;
try {
currentMode = await securityModeService.getCurrentMode();
} catch (error) {
// Ignorer les erreurs de base de données lors du premier lancement
console.log('🔐 No security mode configured yet (first launch)');
currentMode = null;
} }
// Always trigger WebAuthn flow for authentication if (!currentMode) {
console.log('🔐 Triggering WebAuthn authentication...'); // Aucun mode sélectionné, afficher le sélecteur
console.log('🔐 No security mode selected, showing selector...');
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Please select your security mode...</span>';
}
// Import and trigger WebAuthn directly // Réinitialiser le flag avant d'afficher le sélecteur
isPairingInProgress = false;
await showSecurityModeSelector();
return; // La fonction sera rappelée après sélection du mode
}
// Mode sélectionné, continuer avec l'authentification
console.log(`🔐 Using security mode: ${currentMode}`);
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>Authenticating with selected security mode...</span>';
}
// Import and trigger authentication with selected mode
const { secureCredentialsService } = await import('../../services/secure-credentials.service'); const { secureCredentialsService } = await import('../../services/secure-credentials.service');
// Check if we have existing credentials (regardless of wallet existence) // Check if we have existing credentials (regardless of wallet existence)
console.log('🔍 Checking for existing WebAuthn credentials...'); console.log('🔍 Checking for existing credentials...');
const hasCredentials = await secureCredentialsService.hasCredentials(); const hasCredentials = await secureCredentialsService.hasCredentials();
if (hasCredentials) { if (hasCredentials) {
console.log('🔓 Existing WebAuthn credentials found, decrypting...'); console.log('🔓 Existing credentials found, decrypting...');
if (mainStatus) { if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>Decrypting existing credentials...</span>'; mainStatus.innerHTML = '<div class="spinner"></div><span>Decrypting existing credentials...</span>';
} }
// This will trigger WebAuthn for decryption of existing credentials try {
console.log('🔐 Starting WebAuthn decryption process...'); // This will trigger authentication for decryption of existing credentials
await secureCredentialsService.retrieveCredentials(''); console.log('🔐 Starting credentials decryption process...');
console.log('✅ WebAuthn decryption completed'); const decryptedCredentials = await secureCredentialsService.retrieveCredentials('');
if (mainStatus) { if (!decryptedCredentials) {
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Credentials decrypted successfully</span>'; throw new Error('Failed to decrypt existing credentials - no data returned');
}
console.log('✅ Credentials decryption completed successfully');
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Credentials decrypted successfully</span>';
}
} catch (error) {
console.error('❌ Credentials decryption failed:', error);
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Failed to decrypt credentials. Please try again.</span>';
}
throw error; // Arrêter le processus si le déchiffrement échoue
} }
} else { } else {
console.log('🔐 No existing WebAuthn credentials, creating new ones...'); console.log('🔐 No existing credentials, creating new ones...');
if (mainStatus) { if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Setting up secure authentication...</span>'; mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Setting up secure authentication...</span>';
} }
// This will trigger WebAuthn for creation of new credentials try {
console.log('🔐 Starting WebAuthn creation process...'); // This will trigger authentication for creation of new credentials
if (mainStatus) { console.log('🔐 Starting credentials creation process...');
mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Creating secure credentials with your device...</span>'; if (mainStatus) {
} mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Creating secure credentials with your device...</span>';
const credentialData = await secureCredentialsService.generateSecureCredentials(''); }
console.log('✅ WebAuthn creation completed');
// Store the credentials in IndexedDB const credentialData = await secureCredentialsService.generateSecureCredentials('4nk-secure-password');
console.log('💾 Storing credentials in IndexedDB...');
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>💾 Securing credentials...</span>';
}
await secureCredentialsService.storeCredentials(credentialData, '');
console.log('✅ Credentials stored successfully');
// Decrypt and make keys available to SDK if (!credentialData || !credentialData.spendKey || !credentialData.scanKey) {
console.log('🔓 Decrypting credentials for SDK access...'); throw new Error('Failed to generate valid credentials - missing spendKey or scanKey');
if (mainStatus) { }
mainStatus.innerHTML = '<div class="spinner"></div><span>🔓 Making keys available...</span>';
}
await secureCredentialsService.retrieveCredentials('');
console.log('✅ Credentials decrypted and available');
if (mainStatus) { console.log('✅ Credentials creation completed successfully');
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Secure authentication ready</span>';
// Store the credentials in IndexedDB
console.log('💾 Storing credentials in IndexedDB...');
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>💾 Securing credentials...</span>';
}
await secureCredentialsService.storeCredentials(credentialData, '');
console.log('✅ Credentials stored successfully');
// Decrypt and make keys available to SDK
console.log('🔓 Decrypting credentials for SDK access...');
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>🔓 Making keys available...</span>';
}
const retrievedCredentials = await secureCredentialsService.retrieveCredentials('');
if (!retrievedCredentials) {
throw new Error('Failed to retrieve stored credentials - decryption failed');
}
console.log('✅ Credentials decrypted and available');
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Secure authentication ready</span>';
}
} catch (error) {
console.error('❌ Credentials creation/encryption/decryption failed:', error);
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Failed to create/encrypt/decrypt credentials. Please try again.</span>';
}
throw error; // Arrêter le processus si la génération/chiffrement/déchiffrement échoue
} }
} }
@ -523,42 +997,42 @@ async function handleMainPairing(): Promise<void> {
console.log(`🔍 Checking credentials availability (attempt ${attempts}/${maxAttempts})...`); console.log(`🔍 Checking credentials availability (attempt ${attempts}/${maxAttempts})...`);
try { try {
credentialsReady = await secureCredentialsService.hasCredentials(); // Vérifier que les credentials sont réellement disponibles
if (credentialsReady) { const credentials = await secureCredentialsService.retrieveCredentials('');
console.log('✅ Credentials verified, proceeding with pairing...'); if (!credentials || !credentials.spendKey || !credentials.scanKey) {
break; throw new Error('Credentials not properly available');
} else { }
console.log(`⏳ Credentials not ready yet, waiting ${delayMs}ms... (attempt ${attempts}/${maxAttempts})`); credentialsReady = true;
console.log('✅ Credentials verified as available');
} catch (error) {
console.warn(`⚠️ Credentials not ready on attempt ${attempts}:`, error);
if (attempts < maxAttempts) {
console.log(`⏳ Waiting ${delayMs}ms before next attempt...`);
await new Promise(resolve => setTimeout(resolve, delayMs)); await new Promise(resolve => setTimeout(resolve, delayMs));
} }
} catch (error) {
console.warn(`⚠️ Error checking credentials (attempt ${attempts}):`, error);
await new Promise(resolve => setTimeout(resolve, delayMs));
} }
} }
// Si les credentials ne sont toujours pas disponibles après tous les essais, arrêter le processus
if (!credentialsReady) { if (!credentialsReady) {
console.error('❌ Credentials not ready after creation - checking IndexedDB directly...'); console.error('❌ Credentials not available after all attempts - stopping process');
// Try to check IndexedDB directly for debugging
try {
const directCheck = await secureCredentialsService.getEncryptedCredentials();
console.log('🔍 Direct IndexedDB check result:', directCheck);
} catch (error) {
console.error('❌ Direct IndexedDB check failed:', error);
}
if (mainStatus) { if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Failed to create credentials</span>'; mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Authentication failed - credentials not available</span>';
} }
return; throw new Error('Credentials not available after maximum retry attempts');
} }
console.log('✅ Credentials verified, proceeding with pairing...');
// Now proceed with pairing process // Now proceed with pairing process
console.log('🚀 Starting pairing process...'); console.log('🚀 Starting pairing process...');
if (mainStatus) { if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>🚀 Starting secure pairing process...</span>'; mainStatus.innerHTML = '<div class="spinner"></div><span>🚀 Starting secure pairing process...</span>';
} }
// Attendre que les credentials soient réellement disponibles avant de continuer
await waitForCredentialsAvailability();
await prepareAndSendPairingTx(); await prepareAndSendPairingTx();
} catch (error) { } catch (error) {
@ -574,9 +1048,14 @@ async function handleMainPairing(): Promise<void> {
} else { } else {
console.error('Pairing failed:', error); console.error('Pairing failed:', error);
if (mainStatus) { if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--info-color)">⏳ Waiting for user to validate secure key access...</span>'; mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Pairing failed: ' + (error as Error).message + '</span>';
} }
throw error;
} }
} finally {
// Réinitialiser les flags pour permettre de nouveaux appels
isPairingInProgress = false;
// Ne pas réinitialiser pairingAttempts ici pour éviter les boucles infinies
} }
} }

View File

@ -0,0 +1,155 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Configuration de la Sécurité - LeCoffre</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
max-width: 500px;
width: 100%;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 30px;
}
.security-options {
display: flex;
flex-direction: column;
gap: 15px;
}
.security-option {
border: 2px solid #e1e5e9;
border-radius: 8px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
background: #f8f9fa;
}
.security-option:hover {
border-color: #667eea;
background: #f0f2ff;
}
.security-option.selected {
border-color: #667eea;
background: #f0f2ff;
}
.option-title {
font-weight: 600;
color: #333;
margin-bottom: 5px;
}
.option-description {
color: #666;
font-size: 14px;
}
.warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 6px;
padding: 15px;
margin-top: 20px;
color: #856404;
}
.continue-btn {
width: 100%;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
padding: 15px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
margin-top: 20px;
transition: background 0.3s ease;
}
.continue-btn:hover {
background: #5a6fd8;
}
.continue-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="container">
<h1>🔐 Configuration de la Sécurité</h1>
<p class="subtitle">Choisissez votre mode de sécurité pour protéger vos clés</p>
<div class="security-options">
<div class="security-option" data-mode="proton-pass">
<div class="option-title">🛡️ Clé de sécurité</div>
<div class="option-description">Sécurité maximale avec votre gestionnaire de clés de sécurité</div>
</div>
<div class="security-option" data-mode="os">
<div class="option-title">🔒 Système d'exploitation</div>
<div class="option-description">Utilise l'authentification biométrique de votre système</div>
</div>
<div class="security-option" data-mode="browser">
<div class="option-title">🌐 Navigateur</div>
<div class="option-description">Sauvegarde dans le gestionnaire de mots de passe du navigateur</div>
</div>
<div class="security-option" data-mode="otp">
<div class="option-title">🔐 OTP (code à usage unique)</div>
<div class="option-description">Code OTP généré par Proton Pass, Google Authenticator, etc.</div>
</div>
<div class="security-option" data-mode="password">
<div class="option-title">🔑 Mot de passe</div>
<div class="option-description">Chiffrement par mot de passe (non récupérable si oublié)</div>
</div>
<div class="security-option" data-mode="none">
<div class="option-title">⚠️ Aucune protection</div>
<div class="option-description">Stockage en clair (non recommandé)</div>
</div>
</div>
<div class="warning" id="warning" style="display: none;">
⚠️ <strong>Attention :</strong> Ce mode de sécurité n'est pas recommandé. Vos clés seront stockées en clair.
</div>
<button class="continue-btn" id="continueBtn" disabled>Continuer</button>
</div>
<script type="module" src="./security-setup.ts"></script>
</body>
</html>

View File

@ -0,0 +1,99 @@
/**
* Page de configuration de la sécurité
* Première étape du processus d'initialisation
*/
import { SecurityMode } from '../../services/security-mode.service';
let selectedMode: SecurityMode | null = null;
document.addEventListener('DOMContentLoaded', () => {
console.log('🔐 Security setup page loaded');
const options = document.querySelectorAll('.security-option');
const continueBtn = document.getElementById('continueBtn') as HTMLButtonElement;
const warning = document.getElementById('warning') as HTMLDivElement;
// Gestion de la sélection des options
options.forEach(option => {
option.addEventListener('click', () => {
// Désélectionner toutes les options
options.forEach(opt => opt.classList.remove('selected'));
// Sélectionner l'option cliquée
option.classList.add('selected');
// Récupérer le mode sélectionné
const mode = option.getAttribute('data-mode') as SecurityMode;
selectedMode = mode;
// Afficher l'avertissement pour les modes non sécurisés
if (mode === 'none') {
warning.style.display = 'block';
} else {
warning.style.display = 'none';
}
// Activer le bouton continuer
continueBtn.disabled = false;
console.log('🔐 Security mode selected:', mode);
});
});
// Gestion du bouton continuer
continueBtn.addEventListener('click', async () => {
if (!selectedMode) {
console.error('❌ No security mode selected');
return;
}
try {
console.log('🔐 Processing security mode:', selectedMode);
// Sauvegarder le mode de sécurité
const { SecurityModeService } = await import('../../services/security-mode.service');
const securityModeService = SecurityModeService.getInstance();
await securityModeService.setSecurityMode(selectedMode);
console.log('✅ Security mode saved successfully');
console.log('🔐 Generating PBKDF2 key for security mode:', selectedMode);
// Désactiver le bouton pendant la génération
continueBtn.disabled = true;
continueBtn.textContent = 'Génération de la clé...';
try {
// Générer la clé PBKDF2 selon le mode choisi
const { SecureCredentialsService } = await import('../../services/secure-credentials.service');
const secureCredentialsService = SecureCredentialsService.getInstance();
console.log('🔐 Generating PBKDF2 key with security mode:', selectedMode);
// Générer la clé PBKDF2 et la stocker selon le mode
const pbkdf2Key = await secureCredentialsService.generatePBKDF2Key(selectedMode);
console.log('✅ PBKDF2 key generated and stored securely');
// Rediriger vers la page de génération du wallet
window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
} catch (error) {
console.error('❌ PBKDF2 key generation failed:', error);
alert('Erreur lors de la génération de la clé de sécurité. Veuillez réessayer.');
// Réactiver le bouton
continueBtn.disabled = false;
continueBtn.textContent = 'Continuer';
}
} catch (error) {
console.error('❌ Error processing security mode:', error);
alert('Erreur lors du traitement du mode de sécurité');
// Réactiver le bouton
continueBtn.disabled = false;
continueBtn.textContent = 'Continuer';
}
});
});

View File

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Génération du Wallet - LeCoffre</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
max-width: 500px;
width: 100%;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 30px;
}
.status {
text-align: center;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.status.loading {
background: #e3f2fd;
color: #1976d2;
}
.status.success {
background: #e8f5e8;
color: #2e7d32;
}
.status.error {
background: #ffebee;
color: #c62828;
}
.progress {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin: 20px 0;
}
.progress-bar {
height: 100%;
background: #667eea;
width: 0%;
transition: width 0.3s ease;
}
.continue-btn {
width: 100%;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
padding: 15px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
margin-top: 20px;
transition: background 0.3s ease;
}
.continue-btn:hover {
background: #5a6fd8;
}
.continue-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="container">
<h1>💰 Génération du Wallet</h1>
<p class="subtitle">Création et sécurisation de votre portefeuille</p>
<div class="status loading" id="status">
🔄 Initialisation en cours...
</div>
<div class="progress">
<div class="progress-bar" id="progressBar"></div>
</div>
<button class="continue-btn" id="continueBtn" disabled>Continuer vers le Pairing</button>
</div>
<script type="module" src="./wallet-setup.ts"></script>
</body>
</html>

View File

@ -0,0 +1,377 @@
/**
* Page de génération du wallet
* Deuxième étape du processus d'initialisation
*/
document.addEventListener('DOMContentLoaded', async () => {
console.log('💰 Wallet setup page loaded');
const status = document.getElementById('status') as HTMLDivElement;
const progressBar = document.getElementById('progressBar') as HTMLDivElement;
const continueBtn = document.getElementById('continueBtn') as HTMLButtonElement;
function updateStatus(message: string, type: 'loading' | 'success' | 'error') {
status.textContent = message;
status.className = `status ${type}`;
}
function updateProgress(percent: number) {
progressBar.style.width = `${percent}%`;
}
// Méthode pour sauvegarder directement en IndexedDB dans la base 4nk
async function saveCredentialsDirectly(credentials: any): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open('4nk', 2);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['wallet'], 'readwrite');
const store = transaction.objectStore('wallet');
const putRequest = store.put(credentials, '/4nk/credentials');
putRequest.onsuccess = () => resolve();
putRequest.onerror = () => reject(putRequest.error);
};
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains('wallet')) {
db.createObjectStore('wallet');
}
};
});
}
try {
// Étape 1: Initialisation des services
updateStatus('🔄 Initialisation des services...', 'loading');
updateProgress(10);
let services; // Déclarer services au niveau supérieur
try {
console.log('🔄 Importing services...');
const serviceModule = await import('../../services/service');
console.log('✅ Service module imported:', Object.keys(serviceModule));
// La classe Services est exportée par défaut
const Services = serviceModule.default;
if (!Services) {
throw new Error('Services class not found in default export');
}
console.log('🔄 Waiting for services to be ready...');
// Attendre que les services soient initialisés avec plus de patience
let attempts = 0;
const maxAttempts = 30; // Plus de tentatives
const delayMs = 2000; // Délai plus long entre les tentatives
while (attempts < maxAttempts) {
try {
console.log(`🔄 Attempting to get services (attempt ${attempts + 1}/${maxAttempts})...`);
services = await Services.getInstance();
console.log('✅ Services initialized successfully');
break;
} catch (error) {
console.log(`⏳ Services not ready yet (attempt ${attempts + 1}/${maxAttempts}):`, error.message);
// Diagnostic plus détaillé
if (attempts === 5) {
console.log('🔍 Diagnostic: Checking memory usage...');
if ((performance as any).memory) {
const memory = (performance as any).memory;
console.log(`📊 Memory usage: ${Math.round(memory.usedJSHeapSize / 1024 / 1024)}MB / ${Math.round(memory.totalJSHeapSize / 1024 / 1024)}MB`);
}
}
attempts++;
if (attempts >= maxAttempts) {
throw new Error(`Services failed to initialize after ${maxAttempts} attempts. This may be due to high memory usage or WebAssembly initialization issues.`);
}
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
} catch (error) {
console.error('❌ Services not available:', error);
throw error;
}
// Étape 2: Génération des credentials sécurisés
updateStatus('🔐 Génération des clés de sécurité...', 'loading');
updateProgress(30);
try {
const { SecureCredentialsService } = await import('../../services/secure-credentials.service');
const secureCredentialsService = SecureCredentialsService.getInstance();
// Vérifier si des credentials existent déjà
const hasCredentials = await secureCredentialsService.hasCredentials();
console.log('🔐 Has existing credentials:', hasCredentials);
if (!hasCredentials) {
updateStatus('🔐 Génération des clés de sécurité...', 'loading');
const { SecurityModeService } = await import('../../services/security-mode.service');
const securityModeService = SecurityModeService.getInstance();
const currentMode = await securityModeService.getCurrentMode();
console.log('🔐 Current security mode:', currentMode);
if (currentMode) {
// Générer la clé PBKDF2 avec le mode de sécurité choisi
updateStatus('🔐 Génération de la clé de chiffrement...', 'loading');
const pbkdf2Key = await secureCredentialsService.generatePBKDF2Key(currentMode);
console.log('✅ PBKDF2 key generated for mode:', currentMode);
// Générer les credentials avec le mode de sécurité choisi
const credentials = await secureCredentialsService.generateSecureCredentials('4nk-secure-password');
console.log('✅ Secure credentials generated');
} else {
// Fallback au mode browser si aucun mode n'est défini
console.log('⚠️ No security mode found, using browser mode');
const credentials = await secureCredentialsService.generateSecureCredentials('4nk-secure-password');
console.log('✅ Secure credentials generated with browser mode');
}
} else {
console.log('✅ Secure credentials already exist');
}
} catch (error) {
console.warn('⚠️ Service worker not ready, credentials will be saved later:', error);
// Pas de fallback localStorage - les credentials seront sauvegardés plus tard
}
// Étape 3: Sauvegarde du wallet avec état birthday_waiting
updateStatus('💰 Sauvegarde du portefeuille...', 'loading');
updateProgress(60);
try {
console.log('🔐 Sauvegarde du wallet avec état birthday_waiting...');
// Récupérer le mode de sécurité pour le chiffrement
const { SecurityModeService } = await import('../../services/security-mode.service');
const securityModeService = SecurityModeService.getInstance();
let currentMode = await securityModeService.getCurrentMode();
// Si aucun mode n'est trouvé, utiliser le mode par défaut
if (!currentMode) {
console.log('⚠️ No security mode found, using browser mode as fallback');
currentMode = 'browser';
}
console.log('🔐 Using security mode for wallet encryption:', currentMode);
// Générer un wallet temporaire avec état birthday_waiting
const { StorageService } = await import('../../services/credentials/storage.service');
const { EncryptionService } = await import('../../services/credentials/encryption.service');
const storageService = StorageService.getInstance();
const encryptionService = EncryptionService.getInstance();
// Générer des clés temporaires pour le wallet
// Générer un wallet temporaire avec l'état birthday_waiting
const walletData = {
scan_sk: encryptionService.generateRandomKey(),
spend_key: encryptionService.generateRandomKey(),
network: 'signet',
state: 'birthday_waiting',
created_at: new Date().toISOString()
};
console.log('🔐 Wallet data generated:', walletData);
// Récupérer la clé PBKDF2 générée par le service de sécurité
const { SecureCredentialsService } = await import('../../services/secure-credentials.service');
const secureCredentialsService = SecureCredentialsService.getInstance();
// Générer la clé PBKDF2 avec le mode de sécurité choisi
const pbkdf2Key = await secureCredentialsService.generatePBKDF2Key(currentMode);
console.log('🔐 PBKDF2 key retrieved for wallet encryption');
// Chiffrer le wallet avec la clé PBKDF2
const encryptedWallet = await encryptionService.encryptWithPassword(
JSON.stringify(walletData),
pbkdf2Key
);
console.log('🔐 Wallet encrypted with PBKDF2 key');
console.log('🔐 Encrypted wallet data:', encryptedWallet);
// Ouvrir la base de données 4nk existante sans la modifier
console.log('🔍 Opening IndexedDB database "4nk" version 2...');
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open('4nk', 2); // Utiliser la version existante
request.onerror = () => {
console.error('❌ Failed to open IndexedDB:', request.error);
reject(request.error);
};
request.onsuccess = () => {
console.log('✅ IndexedDB opened successfully');
console.log('🔍 Database name:', request.result.name);
console.log('🔍 Database version:', request.result.version);
console.log('🔍 Available stores:', Array.from(request.result.objectStoreNames));
resolve(request.result);
};
request.onupgradeneeded = () => {
const db = request.result;
console.log('🔄 IndexedDB upgrade needed, checking wallet store...');
// Créer le store wallet seulement s'il n'existe pas
if (!db.objectStoreNames.contains('wallet')) {
const store = db.createObjectStore('wallet', { keyPath: 'pre_id' });
console.log('✅ Wallet store created with keyPath: pre_id');
} else {
console.log('✅ Wallet store already exists');
}
};
});
// Étape 1: Sauvegarder le wallet dans le format attendu par getDeviceFromDatabase
console.log('🔍 Opening transaction for wallet store...');
await new Promise<void>((resolve, reject) => {
const transaction = db.transaction(['wallet'], 'readwrite');
const store = transaction.objectStore('wallet');
console.log('🔍 Store opened:', store.name);
// Créer un device temporaire avec l'état birthday_waiting
// Utiliser le wallet chiffré au lieu des données en clair
const device = {
sp_wallet: {
// Stocker le wallet chiffré au lieu des clés en clair
encrypted_data: encryptedWallet,
network: walletData.network,
birthday: 0, // Sera mis à jour dans birthday-setup
last_scan: 0,
state: 'birthday_waiting'
},
sp_client: {
// Structure complète pour éviter l'erreur "missing field sp_client"
initialized: false,
// Ajouter d'autres champs nécessaires
version: 1,
capabilities: []
},
created_at: walletData.created_at
};
// Stocker dans le format attendu par getDeviceFromDatabase
const walletObject = {
pre_id: '1',
device: device
};
console.log('🔍 Attempting to save wallet object:', walletObject);
// Le store utilise des clés out-of-line, fournir une clé explicite
const request = store.put(walletObject, '1');
request.onsuccess = () => {
console.log('✅ Wallet saved in IndexedDB with correct format');
console.log('🔍 Saved wallet object:', walletObject);
resolve();
};
request.onerror = () => {
console.error('❌ Failed to save wallet in IndexedDB:', request.error);
reject(request.error);
};
transaction.oncomplete = () => {
console.log('✅ Transaction completed successfully');
console.log('🔍 Transaction completed for store:', store.name);
};
transaction.onerror = () => {
console.error('❌ Transaction failed:', transaction.error);
console.error('🔍 Transaction error details:', {
error: transaction.error,
store: store.name,
mode: transaction.mode
});
reject(transaction.error);
};
});
// Étape 2: Vérifier que le wallet est bien stocké (nouvelle transaction)
await new Promise<void>((resolve, reject) => {
const transaction = db.transaction(['wallet'], 'readonly');
const store = transaction.objectStore('wallet');
const verificationRequest = store.get('1');
verificationRequest.onsuccess = () => {
console.log('🔍 Verification result:', verificationRequest.result);
if (verificationRequest.result) {
console.log('✅ Wallet verification: Found in IndexedDB with key "1"');
console.log('🔍 Device state:', verificationRequest.result.device.sp_wallet.state);
console.log('🔍 Encrypted data present:', !!verificationRequest.result.device.sp_wallet.encrypted_data);
resolve();
} else {
console.error('❌ Wallet verification: Not found in IndexedDB with key "1"');
reject(new Error('Wallet not found after save'));
}
};
verificationRequest.onerror = () => {
console.error('❌ Wallet verification failed:', verificationRequest.error);
reject(verificationRequest.error);
};
});
// Sauvegarder le mode de sécurité choisi
console.log('🔐 Saving security mode:', currentMode);
await securityModeService.setSecurityMode(currentMode);
// Vérifier que le mode de sécurité est bien sauvegardé
const savedMode = await securityModeService.getCurrentMode();
if (savedMode === currentMode) {
console.log('✅ Security mode saved and verified:', savedMode);
} else {
console.error('❌ Security mode verification failed. Expected:', currentMode, 'Got:', savedMode);
throw new Error('Security mode verification failed');
}
// Vérification finale : s'assurer que le wallet est bien dans IndexedDB
console.log('🔍 Final verification: checking wallet in IndexedDB...');
const finalVerification = await new Promise<any>((resolve, reject) => {
const transaction = db.transaction(['wallet'], 'readonly');
const store = transaction.objectStore('wallet');
const request = store.get('1');
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
if (finalVerification && finalVerification.device) {
console.log('✅ Wallet saved exclusively in IndexedDB and verified');
console.log('🔍 Wallet contains:', {
hasSpWallet: !!finalVerification.device.sp_wallet,
hasSpClient: !!finalVerification.device.sp_client,
state: finalVerification.device.sp_wallet?.state,
hasEncryptedData: !!finalVerification.device.sp_wallet?.encrypted_data
});
} else {
console.error('❌ Final wallet verification failed - wallet not found in IndexedDB');
throw new Error('Wallet verification failed - wallet not found');
}
} catch (error) {
console.error('❌ Error during wallet save:', error);
updateStatus('❌ Erreur: Échec de la sauvegarde du wallet', 'error');
throw error;
}
// Étape 4: Finalisation
updateStatus('✅ Wallet sauvegardé avec succès!', 'success');
updateProgress(100);
console.log('🎉 Wallet setup completed successfully - wallet saved with birthday_waiting state');
console.log('🔗 Ready to proceed to network connection and birthday setup');
// Activer le bouton continuer
continueBtn.disabled = false;
} catch (error) {
console.error('❌ Error during wallet setup:', error);
updateStatus('❌ Erreur lors de la génération du wallet', 'error');
}
// Gestion du bouton continuer
continueBtn.addEventListener('click', () => {
console.log('🔗 Redirecting to pairing page...');
window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
});
});

View File

@ -138,6 +138,19 @@ export async function init(): Promise<void> {
try { try {
console.log('🚀 Starting application initialization...'); console.log('🚀 Starting application initialization...');
// ÉTAPE 1: GESTION DE LA SÉCURITÉ (clés de sécurité) - EN PREMIER
console.log('🔐 Step 1: Security key management...');
const securityConfigured = await handleSecurityKeyManagement();
if (!securityConfigured) {
console.log('🔐 Security not configured, redirecting to home for setup...');
// Naviguer directement vers home pour la configuration de sécurité
handleLocation('home');
return;
}
// ÉTAPE 2: INITIALISATION DES SERVICES (seulement après la sécurité)
console.log('🔧 Step 2: Initializing services...');
const services = await Services.getInstance(); const services = await Services.getInstance();
(window as any).myService = services; (window as any).myService = services;
const db = await Database.getInstance(); const db = await Database.getInstance();
@ -146,6 +159,29 @@ export async function init(): Promise<void> {
console.log('📱 Registering service worker...'); console.log('📱 Registering service worker...');
await db.registerServiceWorker('/src/service-workers/database.worker.js'); await db.registerServiceWorker('/src/service-workers/database.worker.js');
// ÉTAPE 3: CONNEXION AUX RELAIS (pour la hauteur de blocs)
console.log('🌐 Step 3: Connecting to relays for block height...');
try {
console.log('🌐 Connecting to relays...');
services.updateUserStatus('🌐 Connecting to blockchain relays...');
await services.connectAllRelays();
console.log('✅ Relays connected successfully');
services.updateUserStatus('✅ Connected to blockchain relays');
// CRITICAL: Wait for handshake to be processed and block height to be set
console.log('⏳ Waiting for relay handshake to complete...');
services.updateUserStatus('⏳ Waiting for blockchain synchronization...');
await services.waitForBlockHeight();
console.log('✅ Block height received from relay');
services.updateUserStatus('✅ Blockchain synchronized');
} catch (error) {
console.warn('⚠️ Failed to connect to some relays:', error);
console.log('🔄 Continuing despite relay connection issues...');
services.updateUserStatus('⚠️ Some relays unavailable, continuing...');
}
// ÉTAPE 4: GÉNÉRATION/CHARGEMENT DU WALLET
console.log('💰 Step 4: Wallet generation/loading...');
const device = await services.getDeviceFromDatabase(); const device = await services.getDeviceFromDatabase();
console.log('🚀 ~ device:', device); console.log('🚀 ~ device:', device);
@ -163,46 +199,24 @@ export async function init(): Promise<void> {
services.updateUserStatus('✅ Wallet restored successfully'); services.updateUserStatus('✅ Wallet restored successfully');
} }
// Restore data from database (these operations can fail, so we handle them separately) // CRITICAL: Now that block height is set, synchronize wallet
try { console.log('🔄 Synchronizing wallet with blockchain...');
console.log('📊 Restoring processes from database...'); services.updateUserStatus('🔄 Synchronizing wallet with blockchain...');
await services.restoreProcessesFromDB(); await services.updateDeviceBlockHeight();
} catch (error) { console.log('✅ Wallet synchronization completed');
console.warn('⚠️ Failed to restore processes from database:', error); services.updateUserStatus('✅ Wallet synchronized successfully');
}
try { // ÉTAPE 5: HANDSHAKE
console.log('🔐 Restoring secrets from database...'); console.log('🤝 Step 5: Performing handshake...');
await services.restoreSecretsFromDB(); await performHandshake(services);
} catch (error) {
console.warn('⚠️ Failed to restore secrets from database:', error);
}
try { // ÉTAPE 6: PAIRING
console.log('🌐 Connecting to relays...'); console.log('🔗 Step 6: Device pairing...');
services.updateUserStatus('🌐 Connecting to blockchain relays...'); await handlePairing(services);
await services.connectAllRelays();
console.log('✅ Relays connected successfully');
services.updateUserStatus('✅ Connected to blockchain relays');
// CRITICAL: Wait for handshake to be processed and block height to be set // ÉTAPE 7: ÉCOUTE DES PROCESSUS
console.log('⏳ Waiting for relay handshake to complete...'); console.log('👂 Step 7: Starting process listening...');
services.updateUserStatus('⏳ Waiting for blockchain synchronization...'); await startProcessListening(services);
await services.waitForBlockHeight();
console.log('✅ Block height received from relay');
services.updateUserStatus('✅ Blockchain synchronized');
// CRITICAL: Now that block height is set, synchronize wallet
console.log('🔄 Synchronizing wallet with blockchain...');
services.updateUserStatus('🔄 Synchronizing wallet with blockchain...');
await services.updateDeviceBlockHeight();
console.log('✅ Wallet synchronization completed');
services.updateUserStatus('✅ Wallet synchronized successfully');
} catch (error) {
console.warn('⚠️ Failed to connect to some relays:', error);
console.log('🔄 Continuing despite relay connection issues...');
services.updateUserStatus('⚠️ Some relays unavailable, continuing...');
}
// We register all the event listeners if we run in an iframe // We register all the event listeners if we run in an iframe
if (window.self !== window.top) { if (window.self !== window.top) {
@ -1187,3 +1201,124 @@ document.addEventListener('navigate', (e: Event) => {
} }
} }
}); });
/**
* ÉTAPE 2: Gestion de la sécurité (clés de sécurité)
* Cette étape doit être la première et rien d'autre ne doit s'exécuter en parallèle
*/
async function handleSecurityKeyManagement(): Promise<boolean> {
console.log('🔐 Starting security key management...');
try {
// Vérifier d'abord si un mode de sécurité est configuré
const { SecurityModeService } = await import('./services/security-mode.service');
const securityModeService = SecurityModeService.getInstance();
const currentMode = await securityModeService.getCurrentMode();
if (!currentMode) {
console.log('🔐 No security mode configured, redirecting to security setup...');
window.location.href = '/src/pages/security-setup/security-setup.html';
return false;
}
console.log('🔐 Security mode configured:', currentMode);
// Vérifier si des credentials existent
const { SecureCredentialsService } = await import('./services/secure-credentials.service');
const secureCredentialsService = SecureCredentialsService.getInstance();
const hasCredentials = await secureCredentialsService.hasCredentials();
if (!hasCredentials) {
console.log('🔐 No security credentials found, redirecting to wallet setup...');
window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
return false;
} else {
console.log('🔐 Security credentials found, verifying access...');
// Vérifier l'accès aux credentials
const credentials = await secureCredentialsService.retrieveCredentials('4nk-secure-password');
if (!credentials) {
console.log('❌ Failed to access security credentials');
window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
return false;
}
console.log('✅ Security credentials verified');
return true;
}
} catch (error) {
console.error('❌ Security key management failed:', error);
console.log('🔐 Redirecting to security setup...');
window.location.href = '/src/pages/security-setup/security-setup.html';
return false;
}
}
/**
* ÉTAPE 5: Handshake
*/
async function performHandshake(services: any): Promise<void> {
console.log('🤝 Performing handshake...');
try {
// Le handshake est déjà fait lors de la connexion aux relais
// Cette fonction peut être étendue pour des handshakes supplémentaires
console.log('✅ Handshake completed');
} catch (error) {
console.error('❌ Handshake failed:', error);
throw error;
}
}
/**
* ÉTAPE 6: Pairing
*/
async function handlePairing(services: any): Promise<void> {
console.log('🔗 Handling device pairing...');
try {
// Vérifier le statut de pairing
const isPaired = services.isPaired();
console.log('🔍 Device pairing status:', isPaired ? 'Paired' : 'Not paired');
if (!isPaired) {
console.log('⚠️ Device not paired, user must complete pairing...');
// Le pairing sera géré par la page home
return;
} else {
console.log('✅ Device is already paired');
}
} catch (error) {
console.error('❌ Pairing handling failed:', error);
throw error;
}
}
/**
* ÉTAPE 7: Écoute des processus
*/
async function startProcessListening(services: any): Promise<void> {
console.log('👂 Starting process listening...');
try {
// Restore data from database (these operations can fail, so we handle them separately)
try {
console.log('📊 Restoring processes from database...');
await services.restoreProcessesFromDB();
} catch (error) {
console.warn('⚠️ Failed to restore processes from database:', error);
}
try {
console.log('🔐 Restoring secrets from database...');
await services.restoreSecretsFromDB();
} catch (error) {
console.warn('⚠️ Failed to restore secrets from database:', error);
}
console.log('✅ Process listening started');
} catch (error) {
console.error('❌ Process listening failed:', error);
throw error;
}
}

View File

@ -0,0 +1,52 @@
# Services de Credentials
Cette structure modulaire organise les services de gestion des credentials de manière claire et maintenable.
## Structure
```
credentials/
├── index.ts # Point d'entrée centralisé
├── types.ts # Types et interfaces
├── webauthn.service.ts # Gestion WebAuthn
├── encryption.service.ts # Gestion du chiffrement
├── storage.service.ts # Gestion du stockage
└── README.md # Documentation
```
## Services
### SecureCredentialsService
Service principal qui orchestre tous les autres services. Gère la logique métier et la coordination entre les modules.
### WebAuthnService
- Détection des authentificateurs disponibles
- Création et utilisation de credentials WebAuthn
- Support spécifique pour Proton Pass
### EncryptionService
- Génération de clés aléatoires
- Chiffrement/déchiffrement avec PBKDF2 + AES-GCM
- Chiffrement/déchiffrement avec WebAuthn
### StorageService
- Stockage sécurisé dans IndexedDB
- Gestion des credentials persistants
- Opérations CRUD sur les credentials
## Avantages de cette structure
1. **Séparation des responsabilités** : Chaque service a une responsabilité claire
2. **Maintenabilité** : Code plus facile à comprendre et modifier
3. **Testabilité** : Chaque service peut être testé indépendamment
4. **Réutilisabilité** : Les services peuvent être utilisés séparément
5. **Lisibilité** : Structure claire et organisée
## Utilisation
```typescript
import { SecureCredentialsService } from './credentials';
const service = SecureCredentialsService.getInstance();
const credentials = await service.generateSecureCredentials('password');
```

View File

@ -0,0 +1,32 @@
/**
* Types et interfaces pour la gestion des credentials
* Updated: 2025-10-24 - Fresh file with new name
*/
export interface CredentialData {
spendKey: string;
scanKey: string;
salt: Uint8Array;
iterations: number;
timestamp: number;
webAuthnCredentialId?: string;
webAuthnPublicKey?: number[];
}
export interface CredentialOptions {
iterations?: number;
saltLength?: number;
keyLength?: number;
}
export interface WebAuthnCredential {
id: string;
publicKey: number[];
privateKey?: ArrayBuffer;
}
export interface EncryptionResult {
encryptedData: string;
credentialId: string;
publicKey: number[];
}

View File

@ -0,0 +1,242 @@
/**
* EncryptionService - Gestion du chiffrement des credentials
*/
import { secureLogger } from '../secure-logger';
import { CredentialData, CredentialOptions } from './types';
export class EncryptionService {
private static instance: EncryptionService;
private readonly defaultOptions: Required<CredentialOptions> = {
iterations: 100000,
saltLength: 32,
keyLength: 32
};
private constructor() {}
public static getInstance(): EncryptionService {
if (!EncryptionService.instance) {
EncryptionService.instance = new EncryptionService();
}
return EncryptionService.instance;
}
/**
* Génère des clés aléatoires
*/
generateRandomKeys(): { spendKey: string; scanKey: string } {
const spendKey = crypto.getRandomValues(new Uint8Array(32));
const scanKey = crypto.getRandomValues(new Uint8Array(32));
return {
spendKey: Array.from(spendKey).map(b => b.toString(16).padStart(2, '0')).join(''),
scanKey: Array.from(scanKey).map(b => b.toString(16).padStart(2, '0')).join('')
};
}
/**
* Génère une clé PBKDF2 aléatoire
*/
generateRandomKey(): string {
const keyBytes = crypto.getRandomValues(new Uint8Array(32));
return Array.from(keyBytes).map(b => b.toString(16).padStart(2, '0')).join('');
}
/**
* Chiffre une clé avec un mot de passe
*/
async encryptWithPassword(key: string, password: string): Promise<string> {
try {
const salt = crypto.getRandomValues(new Uint8Array(16));
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveBits']
);
const derivedKey = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
256
);
const cryptoKey = await crypto.subtle.importKey(
'raw',
derivedKey,
{ name: 'AES-GCM' },
false,
['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
cryptoKey,
new TextEncoder().encode(key)
);
// Combiner salt + iv + données chiffrées
const combined = new Uint8Array(salt.length + iv.length + encrypted.byteLength);
combined.set(salt, 0);
combined.set(iv, salt.length);
combined.set(new Uint8Array(encrypted), salt.length + iv.length);
return btoa(String.fromCharCode(...combined));
} catch (error) {
secureLogger.error('Failed to encrypt with password', {
component: 'EncryptionService',
operation: 'encryptWithPassword',
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
/**
* Chiffre des données avec PBKDF2 + AES-GCM (méthode originale)
*/
async encryptWithPasswordOriginal(
data: string,
password: string,
options: CredentialOptions = {}
): Promise<{ encryptedData: string; salt: Uint8Array; iterations: number }> {
const opts = { ...this.defaultOptions, ...options };
const salt = crypto.getRandomValues(new Uint8Array(opts.saltLength));
// Dériver la clé avec PBKDF2
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
const key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: opts.iterations,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
// Chiffrer les données
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
key,
new TextEncoder().encode(data)
);
// Combiner IV + données chiffrées
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
return {
encryptedData: Array.from(combined).map(b => b.toString(16).padStart(2, '0')).join(''),
salt,
iterations: opts.iterations
};
}
/**
* Déchiffre des données avec PBKDF2 + AES-GCM
*/
async decryptWithPassword(
encryptedData: string,
password: string,
salt: Uint8Array,
iterations: number
): Promise<string> {
// Dériver la clé avec PBKDF2
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
const key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: iterations,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
// Convertir les données hexadécimales
const combined = new Uint8Array(encryptedData.match(/.{2}/g)!.map(byte => parseInt(byte, 16)));
// Séparer IV et données chiffrées
const iv = combined.slice(0, 12);
const encrypted = combined.slice(12);
// Déchiffrer
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
key,
encrypted
);
return new TextDecoder().decode(decrypted);
}
/**
* Chiffre des credentials avec WebAuthn
*/
async encryptWithWebAuthn(
credentials: CredentialData,
credentialId: string
): Promise<string> {
const data = JSON.stringify({
spendKey: credentials.spendKey,
scanKey: credentials.scanKey,
timestamp: credentials.timestamp
});
// Pour l'instant, on utilise un chiffrement simple
// Dans une vraie implémentation, on utiliserait la clé publique WebAuthn
const encoded = btoa(data);
return encoded;
}
/**
* Déchiffre des credentials avec WebAuthn
*/
async decryptWithWebAuthn(
encryptedData: string,
credentialId: string
): Promise<CredentialData> {
// Pour l'instant, on utilise un déchiffrement simple
const decoded = atob(encryptedData);
const data = JSON.parse(decoded);
return {
spendKey: data.spendKey,
scanKey: data.scanKey,
salt: new Uint8Array(0),
iterations: 0,
timestamp: data.timestamp
};
}
}

View File

@ -0,0 +1,282 @@
/**
* StorageService - Gestion du stockage des credentials
*/
import { secureLogger } from '../secure-logger';
import { CredentialData } from './types';
export class StorageService {
private static instance: StorageService;
private dbName = '4nk';
private storeName = 'credentials'; // Store séparé pour les clés PBKDF2
private dbVersion = 2;
private constructor() {}
public static getInstance(): StorageService {
if (!StorageService.instance) {
StorageService.instance = new StorageService();
}
return StorageService.instance;
}
/**
* Stocke une clé dans le gestionnaire de mots de passe du navigateur
*/
async storeKeyInBrowser(key: string): Promise<void> {
try {
secureLogger.info('Storing key in browser password manager', {
component: 'StorageService',
operation: 'storeKeyInBrowser'
});
// IMPORTANT: Ne jamais stocker la clé PBKDF2 en clair !
// Utiliser l'API Credential Management pour stocker comme mot de passe
// Le navigateur chiffrera automatiquement le mot de passe
const credential = new PasswordCredential({
id: 'lecoffre-pbkdf2-key',
password: key, // Le navigateur chiffre automatiquement ce mot de passe
name: 'LeCoffre PBKDF2 Key'
});
await navigator.credentials.store(credential);
secureLogger.info('Key stored in browser password manager successfully (encrypted by browser)', {
component: 'StorageService',
operation: 'storeKeyInBrowser'
});
} catch (error) {
secureLogger.error('Failed to store key in browser password manager', {
component: 'StorageService',
operation: 'storeKeyInBrowser',
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
/**
* Stocke un wallet chiffré
*/
async storeEncryptedWallet(encryptedWallet: string): Promise<void> {
try {
secureLogger.info('Storing encrypted wallet', {
component: 'StorageService',
operation: 'storeEncryptedWallet'
});
const db = await this.openDatabase();
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
await new Promise<void>((resolve, reject) => {
const request = store.put(encryptedWallet, 'encrypted-wallet');
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
secureLogger.info('Encrypted wallet stored successfully', {
component: 'StorageService',
operation: 'storeEncryptedWallet'
});
} catch (error) {
secureLogger.error('Failed to store encrypted wallet', {
component: 'StorageService',
operation: 'storeEncryptedWallet',
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
/**
* Stocke une clé en clair (non recommandé)
*/
async storePlainKey(key: string): Promise<void> {
try {
secureLogger.warn('Storing key in plain text (not recommended)', {
component: 'StorageService',
operation: 'storePlainKey'
});
const db = await this.openDatabase();
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
await new Promise<void>((resolve, reject) => {
const request = store.put(key, 'plain-pbkdf2-key');
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
secureLogger.info('Plain key stored successfully', {
component: 'StorageService',
operation: 'storePlainKey'
});
} catch (error) {
secureLogger.error('Failed to store plain key', {
component: 'StorageService',
operation: 'storePlainKey',
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
/**
* Ouvre la base de données IndexedDB
*/
private async openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onerror = () => {
secureLogger.error('Failed to open IndexedDB', new Error('Database open failed'), {
component: 'StorageService',
operation: 'openDatabase'
});
reject(request.error);
};
request.onsuccess = () => {
secureLogger.info('IndexedDB opened successfully', {
component: 'StorageService',
operation: 'openDatabase'
});
resolve(request.result);
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
secureLogger.info('IndexedDB upgrade needed, creating credentials store', {
component: 'StorageService',
operation: 'openDatabase'
});
}
if (!db.objectStoreNames.contains('wallet')) {
db.createObjectStore('wallet');
secureLogger.info('IndexedDB upgrade needed, creating wallet store', {
component: 'StorageService',
operation: 'openDatabase'
});
}
};
});
}
/**
* Stocke des credentials
*/
async storeCredentials(credentials: CredentialData): Promise<void> {
try {
const db = await this.openDatabase();
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
await new Promise<void>((resolve, reject) => {
const request = store.put(credentials, 'current');
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
secureLogger.info('Credentials stored successfully', {
component: 'StorageService',
operation: 'storeCredentials'
});
} catch (error) {
secureLogger.error('Failed to store credentials', error as Error, {
component: 'StorageService',
operation: 'storeCredentials'
});
throw error;
}
}
/**
* Récupère des credentials
*/
async getCredentials(): Promise<CredentialData | null> {
try {
const db = await this.openDatabase();
const transaction = db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
return new Promise<CredentialData | null>((resolve, reject) => {
const request = store.get('current');
request.onsuccess = () => {
const result = request.result;
if (result) {
secureLogger.info('Credentials retrieved successfully', {
component: 'StorageService',
operation: 'getCredentials'
});
resolve(result);
} else {
secureLogger.info('No credentials found', {
component: 'StorageService',
operation: 'getCredentials'
});
resolve(null);
}
};
request.onerror = () => reject(request.error);
});
} catch (error) {
secureLogger.error('Failed to get credentials', error as Error, {
component: 'StorageService',
operation: 'getCredentials'
});
return null;
}
}
/**
* Vérifie si des credentials existent
*/
async hasCredentials(): Promise<boolean> {
try {
const credentials = await this.getCredentials();
return credentials !== null &&
credentials.spendKey !== undefined &&
credentials.scanKey !== undefined;
} catch (error) {
secureLogger.error('Failed to check credentials existence', error as Error, {
component: 'StorageService',
operation: 'hasCredentials'
});
return false;
}
}
/**
* Supprime les credentials
*/
async clearCredentials(): Promise<void> {
try {
const db = await this.openDatabase();
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
await new Promise<void>((resolve, reject) => {
const request = store.delete('current');
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
secureLogger.info('Credentials cleared successfully', {
component: 'StorageService',
operation: 'clearCredentials'
});
} catch (error) {
secureLogger.error('Failed to clear credentials', error as Error, {
component: 'StorageService',
operation: 'clearCredentials'
});
throw error;
}
}
}

View File

@ -0,0 +1,32 @@
/**
* Types et interfaces pour la gestion des credentials
* Updated: 2025-10-24 - Fresh file
*/
export interface CredentialData {
spendKey: string;
scanKey: string;
salt: Uint8Array;
iterations: number;
timestamp: number;
webAuthnCredentialId?: string;
webAuthnPublicKey?: number[];
}
export interface CredentialOptions {
iterations?: number;
saltLength?: number;
keyLength?: number;
}
export interface WebAuthnCredential {
id: string;
publicKey: number[];
privateKey?: ArrayBuffer;
}
export interface EncryptionResult {
encryptedData: string;
credentialId: string;
publicKey: number[];
}

View File

@ -0,0 +1,403 @@
/**
* WebAuthnService - Gestion des opérations WebAuthn
*/
import { secureLogger } from '../secure-logger';
import { SecurityMode } from '../security-mode.service';
import { WebAuthnCredential, EncryptionResult } from './types';
export class WebAuthnService {
private static instance: WebAuthnService;
private constructor() {}
public static getInstance(): WebAuthnService {
if (!WebAuthnService.instance) {
WebAuthnService.instance = new WebAuthnService();
}
return WebAuthnService.instance;
}
/**
* Stocke une clé avec WebAuthn
*/
async storeKeyWithWebAuthn(key: string, securityMode: SecurityMode): Promise<void> {
try {
secureLogger.info('Storing key with WebAuthn', {
component: 'WebAuthnService',
operation: 'storeKeyWithWebAuthn',
securityMode
});
// Créer des credentials WebAuthn pour stocker la clé
const credential = await this.createCredentials('4nk-secure-password', securityMode);
// Stocker la clé chiffrée avec les credentials WebAuthn
const encryptedKey = await this.encryptKeyWithWebAuthn(key, credential);
// Sauvegarder dans IndexedDB
await this.saveEncryptedKey(encryptedKey, credential.id, securityMode);
secureLogger.info('Key stored with WebAuthn successfully', {
component: 'WebAuthnService',
operation: 'storeKeyWithWebAuthn'
});
} catch (error) {
secureLogger.error('Failed to store key with WebAuthn', {
component: 'WebAuthnService',
operation: 'storeKeyWithWebAuthn',
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
/**
* Détecte les authentificateurs disponibles
*/
async detectAvailableAuthenticators(): Promise<boolean> {
try {
if (!navigator.credentials || !navigator.credentials.create) {
return false;
}
if (typeof PublicKeyCredential === 'undefined') {
return false;
}
// Vérifier la disponibilité sans faire d'appel réel à WebAuthn
// Juste vérifier que les APIs sont disponibles
if (!navigator.credentials.get) {
return false;
}
// Vérifier si on est dans un contexte sécurisé (requis pour WebAuthn)
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
return false;
}
return true;
} catch (error) {
secureLogger.error('Error detecting authenticators', error as Error, {
component: 'WebAuthnService',
operation: 'detectAvailableAuthenticators'
});
return false;
}
}
/**
* Détecte spécifiquement Proton Pass
*/
async detectProtonPass(): Promise<boolean> {
try {
console.log('🔍 Detecting Proton Pass availability...');
const available = await this.detectAvailableAuthenticators();
if (!available) {
console.log('❌ WebAuthn not available');
return false;
}
console.log('✅ WebAuthn is available, checking Proton Pass support...');
// Vérifier la disponibilité sans faire d'appel réel à WebAuthn
// Juste vérifier que les APIs sont disponibles
if (!navigator.credentials || !navigator.credentials.create) {
console.log('❌ WebAuthn credentials API not available');
return false;
}
// Vérifier si on est dans un contexte sécurisé (requis pour WebAuthn)
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
console.log('❌ WebAuthn requires HTTPS or localhost');
return false;
}
console.log('✅ Proton Pass should be available (basic checks passed)');
return true;
} catch (error) {
console.log('❌ Error detecting Proton Pass:', error);
return false;
}
}
/**
* Crée des credentials WebAuthn
*/
async createCredentials(
password: string,
mode: SecurityMode
): Promise<WebAuthnCredential> {
console.log('🔐 WebAuthnService.createCredentials called with mode:', mode);
// Vérifier la disponibilité de Proton Pass si c'est le mode sélectionné
if (mode === 'proton-pass') {
console.log('🔍 Checking Proton Pass availability...');
const protonPassAvailable = await this.detectProtonPass();
if (!protonPassAvailable) {
console.log('❌ Proton Pass not available, falling back to platform authenticator');
// Ne pas échouer, mais utiliser un mode de fallback
} else {
console.log('✅ Proton Pass is available and ready');
}
}
const challenge = crypto.getRandomValues(new Uint8Array(32));
const authenticatorSelection: AuthenticatorSelectionCriteria = {
userVerification: "required",
residentKey: "required"
};
// Configuration spécifique selon le mode
if (mode === 'proton-pass') {
authenticatorSelection.authenticatorAttachment = "platform";
console.log('🔐 Configuring for Proton Pass (platform authenticator)');
} else if (mode === 'os') {
authenticatorSelection.authenticatorAttachment = "platform";
console.log('🔐 Configuring for OS authenticator (platform)');
}
const publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions = {
challenge: challenge,
rp: {
name: "4NK Secure Storage",
id: window.location.hostname
},
user: {
id: new TextEncoder().encode(password),
name: "4nk-user",
displayName: "4NK User"
},
pubKeyCredParams: [
{ alg: -7, type: "public-key" },
{ alg: -257, type: "public-key" }
],
authenticatorSelection,
timeout: 60000,
attestation: "none"
};
// Options spécifiques pour Proton Pass
if (mode === 'proton-pass') {
publicKeyCredentialCreationOptions.extensions = {
largeBlob: { support: "preferred" }
};
console.log('🔐 Added largeBlob extension for Proton Pass');
}
console.log('🔐 Calling navigator.credentials.create with options:', publicKeyCredentialCreationOptions);
console.log('🔐 This should trigger Proton Pass window...');
try {
const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions
}) as PublicKeyCredential;
console.log('🔐 WebAuthn credential created successfully:', credential);
return {
id: Array.from(new Uint8Array(credential.rawId)).map(b => b.toString(16).padStart(2, '0')).join(''),
publicKey: Array.from(new Uint8Array((credential.response as AuthenticatorAttestationResponse).publicKey!))
};
} catch (error) {
console.error('❌ WebAuthn credential creation failed:', error);
// Message d'erreur spécifique pour Proton Pass
if (mode === 'proton-pass') {
throw new Error(`Proton Pass authentication failed: ${error.message}. Please ensure Proton Pass is installed and enabled.`);
}
throw error;
}
}
/**
* Chiffre une clé avec WebAuthn
*/
private async encryptKeyWithWebAuthn(key: string, credential: WebAuthnCredential): Promise<string> {
try {
// Utiliser la clé publique WebAuthn pour chiffrer la clé PBKDF2
// Pour l'instant, on utilise un chiffrement AES-GCM avec une clé dérivée
const { EncryptionService } = await import('./encryption.service');
const encryptionService = EncryptionService.getInstance();
// Utiliser l'ID de la credential WebAuthn comme mot de passe pour chiffrer la clé PBKDF2
const encryptedKey = await encryptionService.encryptWithPassword(key, credential.id);
console.log('🔐 Key encrypted with WebAuthn credential');
return encryptedKey;
} catch (error) {
secureLogger.error('Failed to encrypt key with WebAuthn', {
component: 'WebAuthnService',
operation: 'encryptKeyWithWebAuthn',
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
/**
* Sauvegarde une clé chiffrée dans IndexedDB
*/
private async saveEncryptedKey(encryptedKey: string, credentialId: string, securityMode: SecurityMode): Promise<void> {
try {
const db = await this.openDatabase();
const transaction = db.transaction(['webauthn-keys'], 'readwrite');
const store = transaction.objectStore('webauthn-keys');
await new Promise<void>((resolve, reject) => {
// Stocker seulement la clé chiffrée (pas de credentialId au même endroit)
const request = store.put({
encryptedKey,
securityMode: securityMode, // Mode de sécurité dynamique
timestamp: Date.now()
}, 'pbkdf2-key');
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
// Ne pas stocker credentialId - il sera récupéré dynamiquement via WebAuthn
console.log('🔐 CredentialId will be retrieved dynamically via WebAuthn');
} catch (error) {
secureLogger.error('Failed to save encrypted key', {
component: 'WebAuthnService',
operation: 'saveEncryptedKey',
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
/**
* Récupère une clé chiffrée avec WebAuthn (récupération dynamique)
*/
async retrieveKeyWithWebAuthn(): Promise<string | null> {
try {
// Récupérer la clé chiffrée depuis IndexedDB
const db = await this.openDatabase();
const transaction = db.transaction(['webauthn-keys'], 'readonly');
const store = transaction.objectStore('webauthn-keys');
const result = await new Promise<any>((resolve, reject) => {
const request = store.get('pbkdf2-key');
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
if (!result || !result.encryptedKey) {
console.log('🔍 No encrypted key found in WebAuthnKeysDB');
return null;
}
// Récupérer credentialId dynamiquement via WebAuthn
const credentialId = await this.getCurrentCredentialId();
if (!credentialId) {
console.log('🔍 No WebAuthn credential available');
return null;
}
// Déchiffrer la clé avec credentialId
const { EncryptionService } = await import('./encryption.service');
const encryptionService = EncryptionService.getInstance();
const decryptedKey = await encryptionService.decryptWithPassword(result.encryptedKey, credentialId);
console.log('🔐 Key decrypted with WebAuthn credential');
return decryptedKey;
} catch (error) {
secureLogger.error('Failed to retrieve key with WebAuthn', {
component: 'WebAuthnService',
operation: 'retrieveKeyWithWebAuthn',
error: error instanceof Error ? error.message : String(error)
});
return null;
}
}
/**
* Récupère l'ID de la credential WebAuthn actuelle
*/
private async getCurrentCredentialId(): Promise<string | null> {
try {
// Utiliser WebAuthn pour récupérer l'ID de la credential
const credential = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array(32),
allowCredentials: [],
userVerification: 'preferred'
}
});
if (credential && credential.id) {
return credential.id;
}
return null;
} catch (error) {
console.log('🔍 No WebAuthn credential available:', error);
return null;
}
}
/**
* Ouvre la base de données IndexedDB
*/
private async openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open('WebAuthnKeysDB', 1);
request.onerror = () => {
secureLogger.error('Failed to open WebAuthn database', new Error('Database open failed'), {
component: 'WebAuthnService',
operation: 'openDatabase'
});
reject(request.error);
};
request.onsuccess = () => {
resolve(request.result);
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('webauthn-keys')) {
db.createObjectStore('webauthn-keys');
}
};
});
}
/**
* Utilise des credentials WebAuthn existants
*/
async useCredentials(
credentialId: string,
mode: SecurityMode
): Promise<PublicKeyCredential> {
const getOptions: PublicKeyCredentialRequestOptions = {
challenge: crypto.getRandomValues(new Uint8Array(32)),
allowCredentials: [{
id: new TextEncoder().encode(credentialId),
type: 'public-key'
}],
userVerification: 'required',
timeout: 60000,
rpId: window.location.hostname
};
// Configuration spécifique selon le mode
if (mode === 'proton-pass') {
getOptions.extensions = {
largeBlob: { support: "preferred" }
};
}
return await navigator.credentials.get({
publicKey: getOptions
}) as PublicKeyCredential;
}
}

View File

@ -4,7 +4,7 @@ export class Database {
private static instance: Database; private static instance: Database;
private db: IDBDatabase | null = null; private db: IDBDatabase | null = null;
private dbName: string = '4nk'; private dbName: string = '4nk';
private dbVersion: number = 1; private dbVersion: number = 2;
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null; private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
private messageChannel: MessageChannel | null = null; private messageChannel: MessageChannel | null = null;
private messageChannelForGet: MessageChannel | null = null; private messageChannelForGet: MessageChannel | null = null;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,411 @@
/**
* SecurityModeService - Gestion des modes de sécurisation
* Gère le stockage et la récupération du mode de sécurisation choisi par l'utilisateur
*/
import { secureLogger } from './secure-logger';
import Database from './database.service';
export type SecurityMode = 'proton-pass' | 'os' | 'browser' | 'otp' | '2fa' | 'password' | 'none';
export interface SecurityModeConfig {
mode: SecurityMode;
name: string;
description: string;
securityLevel: 'high' | 'medium' | 'low' | 'critical';
requiresConfirmation: boolean;
warnings: string[];
implementation: {
useWebAuthn: boolean;
useEncryption: boolean;
usePlatformAuth: boolean;
storageType: 'encrypted' | 'plain' | 'hybrid';
};
}
export class SecurityModeService {
private static instance: SecurityModeService;
private database: Database;
private currentMode: SecurityMode | null = null;
private constructor() {
this.database = new Database();
}
public static getInstance(): SecurityModeService {
if (!SecurityModeService.instance) {
SecurityModeService.instance = new SecurityModeService();
}
return SecurityModeService.instance;
}
/**
* Récupère le mode de sécurisation actuel
*/
public async getCurrentMode(): Promise<SecurityMode | null> {
if (this.currentMode) {
return this.currentMode;
}
try {
// Vérifier que la base de données est disponible
if (!this.database || typeof this.database.getObject !== 'function') {
secureLogger.warn('Database not available, returning null mode', {
component: 'SecurityModeService',
operation: 'getCurrentMode'
});
return null;
}
const storedMode = await this.database.getObject('security_settings', 'current_mode');
this.currentMode = storedMode?.mode || null;
secureLogger.info('Current security mode retrieved', {
component: 'SecurityModeService',
operation: 'getCurrentMode',
mode: this.currentMode
});
return this.currentMode;
} catch (error) {
// Si l'erreur est "object store not found", c'est normal pour un premier lancement
if (error instanceof Error && (error.name === 'NotFoundError' || error.message.includes('object stores was not found'))) {
secureLogger.info('No security mode set yet (first launch)', {
component: 'SecurityModeService',
operation: 'getCurrentMode'
});
return null;
}
secureLogger.error('Failed to retrieve current security mode', error as Error, {
component: 'SecurityModeService',
operation: 'getCurrentMode'
});
return null;
}
}
/**
* Définit le mode de sécurisation
*/
public async setSecurityMode(mode: SecurityMode): Promise<void> {
try {
const modeConfig = this.getSecurityModeConfig(mode);
// Vérifier que la base de données est disponible
if (!this.database || typeof this.database.setObject !== 'function') {
secureLogger.warn('Database not available, setting mode in memory only', {
component: 'SecurityModeService',
operation: 'setSecurityMode',
mode
});
this.currentMode = mode;
return;
}
// Stocker le mode en base
await this.database.setObject('security_settings', 'current_mode', {
mode,
name: modeConfig.name,
description: modeConfig.description,
securityLevel: modeConfig.securityLevel,
timestamp: Date.now(),
implementation: modeConfig.implementation
});
this.currentMode = mode;
secureLogger.info('Security mode set successfully', {
component: 'SecurityModeService',
operation: 'setSecurityMode',
mode,
securityLevel: modeConfig.securityLevel
});
// Émettre un événement pour notifier les autres services
window.dispatchEvent(new CustomEvent('securityModeChanged', {
detail: { mode, config: modeConfig }
}));
} catch (error) {
secureLogger.error('Failed to set security mode', error as Error, {
component: 'SecurityModeService',
operation: 'setSecurityMode',
mode
});
// En cas d'erreur, définir le mode en mémoire seulement
this.currentMode = mode;
secureLogger.warn('Security mode set in memory only due to database error', {
component: 'SecurityModeService',
operation: 'setSecurityMode',
mode
});
}
}
/**
* Récupère la configuration d'un mode de sécurisation
*/
public getSecurityModeConfig(mode: SecurityMode): SecurityModeConfig {
const configs: Record<SecurityMode, SecurityModeConfig> = {
'proton-pass': {
mode: 'proton-pass',
name: 'Proton Pass',
description: 'Utilise Proton Pass pour l\'authentification biométrique et la gestion des clés',
securityLevel: 'high',
requiresConfirmation: false,
warnings: [],
implementation: {
useWebAuthn: true,
useEncryption: true,
usePlatformAuth: true,
storageType: 'encrypted'
}
},
'os': {
mode: 'os',
name: 'Authentificateur OS',
description: 'Utilise l\'authentificateur intégré de votre système d\'exploitation',
securityLevel: 'high',
requiresConfirmation: false,
warnings: [],
implementation: {
useWebAuthn: true,
useEncryption: true,
usePlatformAuth: true,
storageType: 'encrypted'
}
},
'browser': {
mode: 'browser',
name: 'Navigateur',
description: 'Utilise les fonctionnalités de sécurité du navigateur',
securityLevel: 'medium',
requiresConfirmation: false,
warnings: [],
implementation: {
useWebAuthn: true,
useEncryption: true,
usePlatformAuth: false,
storageType: 'encrypted'
}
},
'otp': {
mode: 'otp',
name: 'OTP (Proton Pass Compatible)',
description: 'Utilise un code OTP généré par une application compatible (Proton Pass, Google Authenticator, etc.)',
securityLevel: 'high',
requiresConfirmation: false,
warnings: [],
implementation: {
useWebAuthn: false,
useEncryption: true,
usePlatformAuth: false,
storageType: 'encrypted',
requiresOTP: true
}
},
'2fa': {
mode: '2fa',
name: 'Application 2FA',
description: 'Stockage en clair avec authentification par application 2FA',
securityLevel: 'low',
requiresConfirmation: true,
warnings: [
'⚠️ Clés stockées en clair',
'⚠️ Risque de compromission',
'⚠️ Non recommandé pour des données sensibles'
],
implementation: {
useWebAuthn: false,
useEncryption: false,
usePlatformAuth: false,
storageType: 'plain'
}
},
'password': {
mode: 'password',
name: 'Mot de Passe (Non Sauvegardé)',
description: 'Vos clés sont chiffrées avec un mot de passe que vous devez saisir à chaque utilisation',
securityLevel: 'low',
requiresConfirmation: true,
warnings: [
'⚠️ Le mot de passe n\'est PAS sauvegardé',
'⚠️ NON récupérable en cas d\'oubli',
'⚠️ À saisir à chaque utilisation'
],
implementation: {
useWebAuthn: false,
useEncryption: true,
usePlatformAuth: false,
storageType: 'encrypted',
requiresPassword: true
}
},
'none': {
mode: 'none',
name: 'Aucune Sécurité',
description: 'Stockage en clair sans aucune protection',
securityLevel: 'critical',
requiresConfirmation: true,
warnings: [
'🚨 Clés stockées en clair',
'🚨 Accès non protégé',
'🚨 RISQUE ÉLEVÉ'
],
implementation: {
useWebAuthn: false,
useEncryption: false,
usePlatformAuth: false,
storageType: 'plain'
}
}
};
return configs[mode];
}
/**
* Vérifie si un mode nécessite une confirmation
*/
public requiresConfirmation(mode: SecurityMode): boolean {
const config = this.getSecurityModeConfig(mode);
return config.requiresConfirmation;
}
/**
* Récupère les avertissements pour un mode
*/
public getWarnings(mode: SecurityMode): string[] {
const config = this.getSecurityModeConfig(mode);
return config.warnings;
}
/**
* Vérifie si le mode actuel est sécurisé
*/
public async isCurrentModeSecure(): Promise<boolean> {
const currentMode = await this.getCurrentMode();
if (!currentMode) return false;
const config = this.getSecurityModeConfig(currentMode);
return config.securityLevel === 'high' || config.securityLevel === 'medium';
}
/**
* Récupère tous les modes disponibles
*/
public getAvailableModes(): SecurityMode[] {
return ['proton-pass', 'os', 'browser', '2fa', 'none'];
}
/**
* Récupère les modes recommandés (sécurisés)
*/
public getRecommendedModes(): SecurityMode[] {
return ['proton-pass', 'os', 'browser'];
}
/**
* Récupère les modes non recommandés (non sécurisés)
*/
public getNonRecommendedModes(): SecurityMode[] {
return ['2fa', 'none'];
}
/**
* Vérifie si un mode utilise WebAuthn
*/
public usesWebAuthn(mode: SecurityMode): boolean {
const config = this.getSecurityModeConfig(mode);
return config.implementation.useWebAuthn;
}
/**
* Vérifie si un mode utilise le chiffrement
*/
public usesEncryption(mode: SecurityMode): boolean {
const config = this.getSecurityModeConfig(mode);
return config.implementation.useEncryption;
}
/**
* Vérifie si un mode utilise l'authentificateur de plateforme
*/
public usesPlatformAuth(mode: SecurityMode): boolean {
const config = this.getSecurityModeConfig(mode);
return config.implementation.usePlatformAuth;
}
/**
* Récupère le type de stockage pour un mode
*/
public getStorageType(mode: SecurityMode): 'encrypted' | 'plain' | 'hybrid' {
const config = this.getSecurityModeConfig(mode);
return config.implementation.storageType;
}
/**
* Réinitialise le mode de sécurisation
*/
public async resetSecurityMode(): Promise<void> {
try {
await this.database.deleteObject('security_settings', 'current_mode');
this.currentMode = null;
secureLogger.info('Security mode reset', {
component: 'SecurityModeService',
operation: 'resetSecurityMode'
});
} catch (error) {
secureLogger.error('Failed to reset security mode', error as Error, {
component: 'SecurityModeService',
operation: 'resetSecurityMode'
});
throw error;
}
}
/**
* Récupère l'historique des modes de sécurisation
*/
public async getSecurityModeHistory(): Promise<any[]> {
try {
const history = await this.database.getObject('security_settings', 'mode_history') || [];
return history;
} catch (error) {
secureLogger.error('Failed to retrieve security mode history', error as Error, {
component: 'SecurityModeService',
operation: 'getSecurityModeHistory'
});
return [];
}
}
/**
* Ajoute une entrée à l'historique des modes
*/
private async addToHistory(mode: SecurityMode): Promise<void> {
try {
const history = await this.getSecurityModeHistory();
history.unshift({
mode,
timestamp: Date.now(),
config: this.getSecurityModeConfig(mode)
});
// Garder seulement les 10 dernières entrées
if (history.length > 10) {
history.splice(10);
}
await this.database.setObject('security_settings', 'mode_history', history);
} catch (error) {
secureLogger.error('Failed to add to security mode history', error as Error, {
component: 'SecurityModeService',
operation: 'addToHistory',
mode
});
}
}
}

View File

@ -176,6 +176,39 @@ export default class Services {
const memory = (performance as any).memory; const memory = (performance as any).memory;
const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100; const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
console.log(`🔍 Initial memory usage: ${usedPercent.toFixed(1)}% (${(memory.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB / ${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(1)}MB)`); console.log(`🔍 Initial memory usage: ${usedPercent.toFixed(1)}% (${(memory.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB / ${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(1)}MB)`);
// Si la mémoire est déjà très élevée, faire un nettoyage agressif immédiat
if (usedPercent > 90) {
console.log('🧹 High memory detected, performing immediate cleanup...');
// Nettoyage agressif immédiat
if (window.gc) {
for (let i = 0; i < 5; i++) {
window.gc();
}
}
// Nettoyer les caches
if ('caches' in window) {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => caches.delete(name)));
}
// Nettoyer localStorage
if (window.localStorage) {
const keys = Object.keys(localStorage);
keys.forEach(key => {
if (key.startsWith('temp_') || key.startsWith('cache_') || key.startsWith('vite_')) {
localStorage.removeItem(key);
}
});
}
// Vérifier la mémoire après nettoyage
const memoryAfter = (performance as any).memory;
const usedPercentAfter = (memoryAfter.usedJSHeapSize / memoryAfter.jsHeapSizeLimit) * 100;
console.log(`📊 Memory after cleanup: ${usedPercentAfter.toFixed(1)}% (${(memoryAfter.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB)`);
}
} }
// Show global loading spinner during initialization // Show global loading spinner during initialization
@ -281,7 +314,7 @@ export default class Services {
const memory = (performance as any).memory; const memory = (performance as any).memory;
const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100; const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
if (usedPercent > 95) { if (usedPercent > 98) {
console.log('🚫 Memory too high, skipping WebAssembly initialization'); console.log('🚫 Memory too high, skipping WebAssembly initialization');
Services.instance = new Services(); Services.instance = new Services();
Services.initializing = null; Services.initializing = null;