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:
parent
c385f23e8f
commit
653c7f32ca
@ -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";
|
||||||
|
|||||||
297
src/components/security-mode-selector/security-mode-selector.css
Normal file
297
src/components/security-mode-selector/security-mode-selector.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
382
src/components/security-mode-selector/security-mode-selector.ts
Normal file
382
src/components/security-mode-selector/security-mode-selector.ts
Normal 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 = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/pages/birthday-setup/birthday-setup.html
Normal file
120
src/pages/birthday-setup/birthday-setup.html
Normal 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>
|
||||||
120
src/pages/birthday-setup/birthday-setup.ts
Normal file
120
src/pages/birthday-setup/birthday-setup.ts
Normal 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}%`;
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -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)
|
||||||
|
if (!isMainPairingSetup) {
|
||||||
setupMainPairing();
|
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...');
|
||||||
|
|
||||||
|
try {
|
||||||
const spAddress = await service.getDeviceAddress();
|
const spAddress = await service.getDeviceAddress();
|
||||||
console.log('🔧 Generating create button...');
|
console.log('🔧 Generating create button...');
|
||||||
generateCreateBtn();
|
generateCreateBtn();
|
||||||
console.log('🔧 Displaying emojis...');
|
console.log('🔧 Displaying emojis...');
|
||||||
displayEmojis(spAddress);
|
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,59 +477,479 @@ 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 (!decryptedCredentials) {
|
||||||
|
throw new Error('Failed to decrypt existing credentials - no data returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Credentials decryption completed successfully');
|
||||||
if (mainStatus) {
|
if (mainStatus) {
|
||||||
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Credentials decrypted successfully</span>';
|
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
|
||||||
|
console.log('🔐 Starting credentials creation process...');
|
||||||
if (mainStatus) {
|
if (mainStatus) {
|
||||||
mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Creating secure credentials with your device...</span>';
|
mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Creating secure credentials with your device...</span>';
|
||||||
}
|
}
|
||||||
const credentialData = await secureCredentialsService.generateSecureCredentials('');
|
|
||||||
console.log('✅ WebAuthn creation completed');
|
const credentialData = await secureCredentialsService.generateSecureCredentials('4nk-secure-password');
|
||||||
|
|
||||||
|
if (!credentialData || !credentialData.spendKey || !credentialData.scanKey) {
|
||||||
|
throw new Error('Failed to generate valid credentials - missing spendKey or scanKey');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Credentials creation completed successfully');
|
||||||
|
|
||||||
// Store the credentials in IndexedDB
|
// Store the credentials in IndexedDB
|
||||||
console.log('💾 Storing credentials in IndexedDB...');
|
console.log('💾 Storing credentials in IndexedDB...');
|
||||||
if (mainStatus) {
|
if (mainStatus) {
|
||||||
mainStatus.innerHTML = '<div class="spinner"></div><span>💾 Securing credentials...</span>';
|
mainStatus.innerHTML = '<div class="spinner"></div><span>💾 Securing credentials...</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
await secureCredentialsService.storeCredentials(credentialData, '');
|
await secureCredentialsService.storeCredentials(credentialData, '');
|
||||||
console.log('✅ Credentials stored successfully');
|
console.log('✅ Credentials stored successfully');
|
||||||
|
|
||||||
@ -496,12 +958,24 @@ async function handleMainPairing(): Promise<void> {
|
|||||||
if (mainStatus) {
|
if (mainStatus) {
|
||||||
mainStatus.innerHTML = '<div class="spinner"></div><span>🔓 Making keys available...</span>';
|
mainStatus.innerHTML = '<div class="spinner"></div><span>🔓 Making keys available...</span>';
|
||||||
}
|
}
|
||||||
await secureCredentialsService.retrieveCredentials('');
|
|
||||||
console.log('✅ Credentials decrypted and available');
|
|
||||||
|
|
||||||
|
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) {
|
if (mainStatus) {
|
||||||
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Secure authentication ready</span>';
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure WebAuthn process is completely finished
|
// Ensure WebAuthn process is completely finished
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
155
src/pages/security-setup/security-setup.html
Normal file
155
src/pages/security-setup/security-setup.html
Normal 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>
|
||||||
99
src/pages/security-setup/security-setup.ts
Normal file
99
src/pages/security-setup/security-setup.ts
Normal 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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
120
src/pages/wallet-setup/wallet-setup.html
Normal file
120
src/pages/wallet-setup/wallet-setup.html
Normal 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>
|
||||||
377
src/pages/wallet-setup/wallet-setup.ts
Normal file
377
src/pages/wallet-setup/wallet-setup.ts
Normal 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';
|
||||||
|
});
|
||||||
|
});
|
||||||
203
src/router.ts
203
src/router.ts
@ -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)
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
// CRITICAL: Now that block height is set, synchronize wallet
|
// CRITICAL: Now that block height is set, synchronize wallet
|
||||||
console.log('🔄 Synchronizing wallet with blockchain...');
|
console.log('🔄 Synchronizing wallet with blockchain...');
|
||||||
services.updateUserStatus('🔄 Synchronizing wallet with blockchain...');
|
services.updateUserStatus('🔄 Synchronizing wallet with blockchain...');
|
||||||
await services.updateDeviceBlockHeight();
|
await services.updateDeviceBlockHeight();
|
||||||
console.log('✅ Wallet synchronization completed');
|
console.log('✅ Wallet synchronization completed');
|
||||||
services.updateUserStatus('✅ Wallet synchronized successfully');
|
services.updateUserStatus('✅ Wallet synchronized successfully');
|
||||||
} catch (error) {
|
|
||||||
console.warn('⚠️ Failed to connect to some relays:', error);
|
// ÉTAPE 5: HANDSHAKE
|
||||||
console.log('🔄 Continuing despite relay connection issues...');
|
console.log('🤝 Step 5: Performing handshake...');
|
||||||
services.updateUserStatus('⚠️ Some relays unavailable, continuing...');
|
await performHandshake(services);
|
||||||
}
|
|
||||||
|
// ÉTAPE 6: PAIRING
|
||||||
|
console.log('🔗 Step 6: Device pairing...');
|
||||||
|
await handlePairing(services);
|
||||||
|
|
||||||
|
// ÉTAPE 7: ÉCOUTE DES PROCESSUS
|
||||||
|
console.log('👂 Step 7: Starting process listening...');
|
||||||
|
await startProcessListening(services);
|
||||||
|
|
||||||
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
52
src/services/credentials/README.md
Normal file
52
src/services/credentials/README.md
Normal 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');
|
||||||
|
```
|
||||||
32
src/services/credentials/credential-types.ts
Normal file
32
src/services/credentials/credential-types.ts
Normal 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[];
|
||||||
|
}
|
||||||
242
src/services/credentials/encryption.service.ts
Normal file
242
src/services/credentials/encryption.service.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
282
src/services/credentials/storage.service.ts
Normal file
282
src/services/credentials/storage.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/services/credentials/types.ts
Normal file
32
src/services/credentials/types.ts
Normal 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[];
|
||||||
|
}
|
||||||
403
src/services/credentials/webauthn.service.ts
Normal file
403
src/services/credentials/webauthn.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
411
src/services/security-mode.service.ts
Normal file
411
src/services/security-mode.service.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user