feat: centralize database configuration and fix service worker blocking

**Motivations :**
- Centralize database configuration to prevent version inconsistencies
- Fix service worker blocking during wallet setup
- Ensure all database stores are created at initialization

**Modifications :**
- Created database-config.ts with centralized DATABASE_CONFIG (name, version, stores)
- Updated storage.service.ts to use DATABASE_CONFIG and create all stores on upgrade
- Updated security-setup.ts to initialize database with complete configuration
- Updated wallet-setup.ts to call SDK directly and bypass service worker blocking
- Updated database.service.ts, webauthn.service.ts, and database.worker.js to use DATABASE_CONFIG
- Removed service worker dependency for wallet setup page

**Pages affected :**
- security-setup.html: Initializes database with all stores on page load
- wallet-setup.html: Saves wallet directly to IndexedDB without service worker dependency
This commit is contained in:
NicolasCantu 2025-10-26 02:19:00 +01:00
parent 653c7f32ca
commit aa913ef930
29 changed files with 463 additions and 291 deletions

View File

@ -30,7 +30,7 @@ export class DeviceManagementComponent extends HTMLElement {
}
async loadDeviceData() {
if (!this.service) return;
if (!this.service) {return;}
try {
// Get current device address and generate 4 words
@ -601,12 +601,12 @@ export class DeviceManagementComponent extends HTMLElement {
const saveBtn = this.shadowRoot!.getElementById('saveChangesBtn') as HTMLButtonElement;
const cancelBtn = this.shadowRoot!.getElementById('cancelChangesBtn') as HTMLButtonElement;
if (saveBtn) saveBtn.disabled = false;
if (cancelBtn) cancelBtn.disabled = false;
if (saveBtn) {saveBtn.disabled = false;}
if (cancelBtn) {cancelBtn.disabled = false;}
}
async saveChanges() {
if (!this.service) return;
if (!this.service) {return;}
try {
// Update the pairing process with new devices
@ -634,8 +634,8 @@ export class DeviceManagementComponent extends HTMLElement {
const saveBtn = this.shadowRoot!.getElementById('saveChangesBtn') as HTMLButtonElement;
const cancelBtn = this.shadowRoot!.getElementById('cancelChangesBtn') as HTMLButtonElement;
if (saveBtn) saveBtn.disabled = true;
if (cancelBtn) cancelBtn.disabled = true;
if (saveBtn) {saveBtn.disabled = true;}
if (cancelBtn) {cancelBtn.disabled = true;}
}
async importAccount() {
@ -692,12 +692,12 @@ export class DeviceManagementComponent extends HTMLElement {
const confirm1 = confirm(
'🚨 EXPORT CRITIQUE: Cette action va exposer votre CLÉ PRIVÉE.\n\nCette clé permet de signer des transactions sans interaction sur le 2ème device.\n\nÊtes-vous sûr de vouloir continuer ?'
);
if (!confirm1) return;
if (!confirm1) {return;}
const confirm2 = confirm(
'⚠️ SÉCURITÉ: Votre clé privée sera visible en clair.\n\nAssurez-vous que personne ne peut voir votre écran.\n\nContinuer ?'
);
if (!confirm2) return;
if (!confirm2) {return;}
const confirm3 = prompt(
'🔐 DERNIÈRE CONFIRMATION: Cette clé privée donne un accès TOTAL à votre compte.\n\nTapez "EXPORTER" pour confirmer:'
@ -711,7 +711,7 @@ export class DeviceManagementComponent extends HTMLElement {
// Get the device's private key
// @ts-ignore - deviceRaw is guaranteed to be non-null after the check below
const deviceRaw = await this.service.getDeviceFromDatabase();
if (!deviceRaw || !deviceRaw.sp_wallet) {
if (!deviceRaw?.sp_wallet) {
throw new Error('Device ou clé privée non trouvée');
}
// TypeScript assertion: deviceRaw is guaranteed to be non-null after the check

View File

@ -325,7 +325,7 @@ export class SecureCredentialsComponent {
*/
private showMessage(message: string, type: 'success' | 'error' | 'warning' | 'info'): void {
const messagesContainer = document.getElementById('credentials-messages');
if (!messagesContainer) return;
if (!messagesContainer) {return;}
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;

View File

@ -309,7 +309,7 @@ export class SecurityModeSelector {
}
private confirmSelection(): void {
if (!this.selectedMode) return;
if (!this.selectedMode) {return;}
const modeConfig = this.getSecurityModes().find(m => m.mode === this.selectedMode);

View File

@ -42,13 +42,13 @@ export async function initValidationModal(processDiffs: any) {
</div>
`;
const box = document.querySelector('.validation-box');
if (box) box.innerHTML += state;
if (box) {box.innerHTML += state;}
}
document.querySelectorAll('.expansion-panel-header').forEach(header => {
header.addEventListener('click', function (event) {
const target = event.target as HTMLElement;
const body = target.nextElementSibling as HTMLElement;
if (body?.style) body.style.display = body.style.display === 'block' ? 'none' : 'block';
if (body?.style) {body.style.display = body.style.display === 'block' ? 'none' : 'block';}
});
});
}

View File

@ -32,7 +32,7 @@ export class LoginComponent extends HTMLElement {
render() {
if (this.shadowRoot)
this.shadowRoot.innerHTML = `
{this.shadowRoot.innerHTML = `
<style>
${loginCss}
</style>${loginHtml}
@ -40,7 +40,7 @@ export class LoginComponent extends HTMLElement {
${loginScript}
</scipt>
`;
`;}
}
}

View File

@ -461,7 +461,7 @@ function setupUserInteractionListener(): void {
let hasTriggered = false;
const triggerWebAuthn = async (event: Event) => {
if (hasTriggered) return;
if (hasTriggered) {return;}
hasTriggered = true;
console.log('🔐 User interaction detected:', event.type, 'triggering WebAuthn...');
@ -812,7 +812,7 @@ async function waitForCredentialsAvailability(): Promise<void> {
// Vérifier que les credentials sont réellement disponibles et accessibles
const credentials = await secureCredentialsService.retrieveCredentials('');
if (credentials && credentials.spendKey && credentials.scanKey) {
if (credentials?.spendKey && credentials.scanKey) {
console.log('✅ Credentials confirmés comme disponibles');
return;
} else {
@ -938,7 +938,7 @@ async function handleMainPairing(): Promise<void> {
const credentialData = await secureCredentialsService.generateSecureCredentials('4nk-secure-password');
if (!credentialData || !credentialData.spendKey || !credentialData.scanKey) {
if (!credentialData?.spendKey || !credentialData.scanKey) {
throw new Error('Failed to generate valid credentials - missing spendKey or scanKey');
}
@ -999,7 +999,7 @@ async function handleMainPairing(): Promise<void> {
try {
// Vérifier que les credentials sont réellement disponibles
const credentials = await secureCredentialsService.retrieveCredentials('');
if (!credentials || !credentials.spendKey || !credentials.scanKey) {
if (!credentials?.spendKey || !credentials.scanKey) {
throw new Error('Credentials not properly available');
}
credentialsReady = true;

View File

@ -4,12 +4,24 @@
*/
import { SecurityMode } from '../../services/security-mode.service';
import { DATABASE_CONFIG, openDatabase } from '../../services/database-config';
let selectedMode: SecurityMode | null = null;
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', async () => {
console.log('🔐 Security setup page loaded');
// Initialiser la base de données avec la configuration complète
try {
console.log(`🔍 Initializing database ${DATABASE_CONFIG.name} version ${DATABASE_CONFIG.version}...`);
await openDatabase();
console.log('✅ Database initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize database:', error);
alert('Erreur lors de l\'initialisation de la base de données. Veuillez réessayer.');
return;
}
const options = document.querySelectorAll('.security-option');
const continueBtn = document.getElementById('continueBtn') as HTMLButtonElement;
const warning = document.getElementById('warning') as HTMLDivElement;
@ -72,6 +84,7 @@ document.addEventListener('DOMContentLoaded', () => {
console.log('🔐 Generating PBKDF2 key with security mode:', selectedMode);
// Générer la clé PBKDF2 et la stocker selon le mode
// IMPORTANT: Cette opération doit être directe (pas de délai) pour que WebAuthn fonctionne
const pbkdf2Key = await secureCredentialsService.generatePBKDF2Key(selectedMode);
console.log('✅ PBKDF2 key generated and stored securely');

View File

@ -3,6 +3,8 @@
* Deuxième étape du processus d'initialisation
*/
import { DATABASE_CONFIG } from '../../services/database-config';
document.addEventListener('DOMContentLoaded', async () => {
console.log('💰 Wallet setup page loaded');
@ -22,13 +24,13 @@ document.addEventListener('DOMContentLoaded', async () => {
// 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);
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['wallet'], 'readwrite');
const store = transaction.objectStore('wallet');
const transaction = db.transaction([DATABASE_CONFIG.stores.wallet.name], 'readwrite');
const store = transaction.objectStore(DATABASE_CONFIG.stores.wallet.name);
const putRequest = store.put(credentials, '/4nk/credentials');
putRequest.onsuccess = () => resolve();
@ -37,8 +39,8 @@ document.addEventListener('DOMContentLoaded', async () => {
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains('wallet')) {
db.createObjectStore('wallet');
if (!db.objectStoreNames.contains(DATABASE_CONFIG.stores.wallet.name)) {
db.createObjectStore(DATABASE_CONFIG.stores.wallet.name, { keyPath: DATABASE_CONFIG.stores.wallet.keyPath as string });
}
};
});
@ -49,7 +51,7 @@ document.addEventListener('DOMContentLoaded', async () => {
updateStatus('🔄 Initialisation des services...', 'loading');
updateProgress(10);
let services; // Déclarer services au niveau supérieur
let services: any; // Déclarer services au niveau supérieur
try {
console.log('🔄 Importing services...');
@ -76,7 +78,7 @@ document.addEventListener('DOMContentLoaded', async () => {
console.log('✅ Services initialized successfully');
break;
} catch (error) {
console.log(`⏳ Services not ready yet (attempt ${attempts + 1}/${maxAttempts}):`, error.message);
console.log(`⏳ Services not ready yet (attempt ${attempts + 1}/${maxAttempts}):`, error instanceof Error ? error.message : String(error));
// Diagnostic plus détaillé
if (attempts === 5) {
@ -198,9 +200,9 @@ document.addEventListener('DOMContentLoaded', async () => {
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...');
console.log(`🔍 Opening IndexedDB database "${DATABASE_CONFIG.name}" version ${DATABASE_CONFIG.version}...`);
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open('4nk', 2); // Utiliser la version existante
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version); // Utiliser la version centralisée
request.onerror = () => {
console.error('❌ Failed to open IndexedDB:', request.error);
reject(request.error);
@ -227,37 +229,48 @@ document.addEventListener('DOMContentLoaded', async () => {
});
// Étape 1: Sauvegarder le wallet dans le format attendu par getDeviceFromDatabase
console.log('🔍 Opening transaction for wallet store...');
// Utiliser le SDK directement pour éviter le service worker qui bloque
console.log('🔧 Creating device using SDK...');
// Créer le device directement avec le SDK sans passer par saveDeviceInDatabase
if (!services.sdkClient) {
throw new Error('WebAssembly SDK not initialized');
}
// We set birthday later when we have the chain tip from relay
console.log('🔧 Creating new device with birthday 0...');
const spAddress = await services.sdkClient.create_new_device(0, 'signet');
console.log('✅ Device created with address:', spAddress);
// Force wallet generation to ensure keys are created
console.log('🔧 Forcing wallet generation...');
try {
const wallet = await services.sdkClient.dump_wallet();
console.log('✅ Wallet generated:', JSON.stringify(wallet));
} catch (walletError) {
console.warn('⚠️ Wallet generation failed:', walletError);
}
// Récupérer le device créé par le SDK
const device = services.dumpDeviceFromMemory();
console.log('🔍 Device structure from SDK:', {
hasSpWallet: !!device.sp_wallet,
hasSpClient: !!device.sp_client,
spClientKeys: device.sp_client ? Object.keys(device.sp_client) : 'none'
});
console.log(`🔍 Opening transaction for ${DATABASE_CONFIG.stores.wallet.name} store...`);
await new Promise<void>((resolve, reject) => {
const transaction = db.transaction(['wallet'], 'readwrite');
const store = transaction.objectStore('wallet');
const transaction = db.transaction([DATABASE_CONFIG.stores.wallet.name], 'readwrite');
const store = transaction.objectStore(DATABASE_CONFIG.stores.wallet.name);
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
// Stocker le wallet chiffré séparément et laisser le SDK gérer sp_wallet
const walletObject = {
pre_id: '1',
device: device
device: device,
encrypted_wallet: encryptedWallet, // Stocker le wallet chiffré séparément
security_mode: currentMode // Stocker le mode de sécurité
};
console.log('🔍 Attempting to save wallet object:', walletObject);
@ -290,8 +303,8 @@ document.addEventListener('DOMContentLoaded', async () => {
// É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 transaction = db.transaction([DATABASE_CONFIG.stores.wallet.name], 'readonly');
const store = transaction.objectStore(DATABASE_CONFIG.stores.wallet.name);
const verificationRequest = store.get('1');
verificationRequest.onsuccess = () => {
@ -328,8 +341,8 @@ document.addEventListener('DOMContentLoaded', async () => {
// 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 transaction = db.transaction([DATABASE_CONFIG.stores.wallet.name], 'readonly');
const store = transaction.objectStore(DATABASE_CONFIG.stores.wallet.name);
const request = store.get('1');
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
@ -340,8 +353,9 @@ document.addEventListener('DOMContentLoaded', async () => {
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
hasEncryptedWallet: !!finalVerification.encrypted_wallet,
securityMode: finalVerification.security_mode,
spClientKeys: finalVerification.device.sp_client ? Object.keys(finalVerification.device.sp_client) : 'none'
});
} else {
console.error('❌ Final wallet verification failed - wallet not found in IndexedDB');

View File

@ -526,7 +526,7 @@ export async function registerAllListeners() {
await services.checkConnections(process, stateId);
let res: Record<string, any> = {};
const res: Record<string, any> = {};
if (state) {
// Decrypt all the data we have the key for
for (const attribute of Object.keys(state.pcd_commitment)) {
@ -621,7 +621,7 @@ export async function registerAllListeners() {
};
const handleGetPairingId = async (event: MessageEvent) => {
if (event.data.type !== MessageType.GET_PAIRING_ID) return;
if (event.data.type !== MessageType.GET_PAIRING_ID) {return;}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
@ -655,7 +655,7 @@ export async function registerAllListeners() {
};
const handleCreateProcess = async (event: MessageEvent) => {
if (event.data.type !== MessageType.CREATE_PROCESS) return;
if (event.data.type !== MessageType.CREATE_PROCESS) {return;}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
@ -704,7 +704,7 @@ export async function registerAllListeners() {
};
const handleNotifyUpdate = async (event: MessageEvent) => {
if (event.data.type !== MessageType.NOTIFY_UPDATE) return;
if (event.data.type !== MessageType.NOTIFY_UPDATE) {return;}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
@ -742,7 +742,7 @@ export async function registerAllListeners() {
};
const handleValidateState = async (event: MessageEvent) => {
if (event.data.type !== MessageType.VALIDATE_STATE) return;
if (event.data.type !== MessageType.VALIDATE_STATE) {return;}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
@ -777,7 +777,7 @@ export async function registerAllListeners() {
};
const handleUpdateProcess = async (event: MessageEvent) => {
if (event.data.type !== MessageType.UPDATE_PROCESS) return;
if (event.data.type !== MessageType.UPDATE_PROCESS) {return;}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
@ -861,7 +861,7 @@ export async function registerAllListeners() {
}
}
if (privateData[field]) continue;
if (privateData[field]) {continue;}
// We've get back all the way to the first state without seeing it, it's a new public field
publicData[field] = newData[field];
@ -888,7 +888,7 @@ export async function registerAllListeners() {
};
const handleDecodePublicData = async (event: MessageEvent) => {
if (event.data.type !== MessageType.DECODE_PUBLIC_DATA) return;
if (event.data.type !== MessageType.DECODE_PUBLIC_DATA) {return;}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
@ -922,7 +922,7 @@ export async function registerAllListeners() {
};
const handleHashValue = async (event: MessageEvent) => {
if (event.data.type !== MessageType.HASH_VALUE) return;
if (event.data.type !== MessageType.HASH_VALUE) {return;}
console.log('handleHashValue', event.data);
@ -951,7 +951,7 @@ export async function registerAllListeners() {
};
const handleGetMerkleProof = async (event: MessageEvent) => {
if (event.data.type !== MessageType.GET_MERKLE_PROOF) return;
if (event.data.type !== MessageType.GET_MERKLE_PROOF) {return;}
try {
const { accessToken, processState, attributeName } = event.data;
@ -978,7 +978,7 @@ export async function registerAllListeners() {
};
const handleValidateMerkleProof = async (event: MessageEvent) => {
if (event.data.type !== MessageType.VALIDATE_MERKLE_PROOF) return;
if (event.data.type !== MessageType.VALIDATE_MERKLE_PROOF) {return;}
try {
const { accessToken, merkleProof, documentHash } = event.data;
@ -1149,7 +1149,7 @@ async function handlePairing4WordsJoin(event: MessageEvent) {
async function cleanPage() {
const container = document.querySelector('#containerId');
if (container) container.innerHTML = '';
if (container) {container.innerHTML = '';}
}
// Essential functions are now handled directly in the application
@ -1191,7 +1191,7 @@ document.addEventListener('navigate', (e: Event) => {
const event = e as CustomEvent<{ page: string; processId?: string }>;
if (event.detail.page === 'chat') {
const container = document.querySelector('.container');
if (container) container.innerHTML = '';
if (container) {container.innerHTML = '';}
//initChat();

View File

@ -101,9 +101,14 @@ async function scanMissingData(processesToScan) {
return Array.from(toDownload);
}
const DATABASE_CONFIG = {
name: '4nk',
version: 3
};
async function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('4nk', 1);
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version);
request.onerror = event => {
reject(request.error);
};
@ -115,6 +120,12 @@ async function openDatabase() {
if (!db.objectStoreNames.contains('wallet')) {
db.createObjectStore('wallet', { keyPath: 'pre_id' });
}
if (!db.objectStoreNames.contains('credentials')) {
db.createObjectStore('credentials');
}
if (!db.objectStoreNames.contains('pbkdf2keys')) {
db.createObjectStore('pbkdf2keys');
}
};
});
}

View File

@ -3,12 +3,13 @@
*/
import { secureLogger } from '../secure-logger';
import { CredentialData } from './types';
import { DATABASE_CONFIG } from '../database-config';
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 dbName = DATABASE_CONFIG.name;
private storeName = DATABASE_CONFIG.stores.credentials.name; // Store séparé pour les clés PBKDF2
private dbVersion = DATABASE_CONFIG.version;
private constructor() {}
@ -150,20 +151,29 @@ export class StorageService {
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'
});
console.log(`🔄 StorageService: Database upgrade needed for ${this.dbName} version ${this.dbVersion}`);
// Créer tous les stores définis dans DATABASE_CONFIG
Object.values(DATABASE_CONFIG.stores).forEach(storeConfig => {
if (!db.objectStoreNames.contains(storeConfig.name)) {
const options: IDBObjectStoreParameters = {};
if (storeConfig.keyPath) {
options.keyPath = storeConfig.keyPath;
}
if (!db.objectStoreNames.contains('wallet')) {
db.createObjectStore('wallet');
secureLogger.info('IndexedDB upgrade needed, creating wallet store', {
component: 'StorageService',
operation: 'openDatabase'
});
if ('autoIncrement' in storeConfig && storeConfig.autoIncrement) {
options.autoIncrement = true;
}
const store = db.createObjectStore(storeConfig.name, options);
// Créer les index
storeConfig.indices.forEach(indexConfig => {
store.createIndex(indexConfig.name, indexConfig.keyPath, { unique: indexConfig.unique || false });
});
console.log(`✅ Created store: ${storeConfig.name}`);
}
});
};
});
}

View File

@ -4,6 +4,7 @@
import { secureLogger } from '../secure-logger';
import { SecurityMode } from '../security-mode.service';
import { WebAuthnCredential, EncryptionResult } from './types';
import { DATABASE_CONFIG } from '../database-config';
export class WebAuthnService {
private static instance: WebAuthnService;
@ -18,35 +19,36 @@ export class WebAuthnService {
}
/**
* Stocke une clé avec WebAuthn
* Stocke une clé PBKDF2 avec WebAuthn
* WebAuthn est utilisé uniquement pour l'authentification, pas pour le chiffrement
*/
async storeKeyWithWebAuthn(key: string, securityMode: SecurityMode): Promise<void> {
try {
secureLogger.info('Storing key with WebAuthn', {
secureLogger.info('Storing PBKDF2 key with WebAuthn encryption', {
component: 'WebAuthnService',
operation: 'storeKeyWithWebAuthn',
securityMode
});
// Créer des credentials WebAuthn pour stocker la clé
const credential = await this.createCredentials('4nk-secure-password', securityMode);
// Créer des credentials WebAuthn pour l'authentification
const credential = await this.createCredentials('4nk-pbkdf2-key', securityMode);
// Stocker la clé chiffrée avec les credentials WebAuthn
// Chiffrer la clé PBKDF2 avec la réponse WebAuthn
const encryptedKey = await this.encryptKeyWithWebAuthn(key, credential);
// Sauvegarder dans IndexedDB
await this.saveEncryptedKey(encryptedKey, credential.id, securityMode);
// Stocker la clé PBKDF2 chiffrée dans IndexedDB
// La clé est stockée avec le securityMode comme clé
await this.savePBKDF2Key(encryptedKey, credential.id, securityMode);
secureLogger.info('Key stored with WebAuthn successfully', {
secureLogger.info('PBKDF2 key encrypted and stored with WebAuthn successfully', {
component: 'WebAuthnService',
operation: 'storeKeyWithWebAuthn'
});
} catch (error) {
secureLogger.error('Failed to store key with WebAuthn', {
secureLogger.error('Failed to store PBKDF2 key with WebAuthn', error as Error, {
component: 'WebAuthnService',
operation: 'storeKeyWithWebAuthn',
error: error instanceof Error ? error.message : String(error)
operation: 'storeKeyWithWebAuthn'
});
throw error;
}
@ -197,16 +199,18 @@ export class WebAuthnService {
console.log('🔐 WebAuthn credential created successfully:', credential);
const response = credential.response as AuthenticatorAttestationResponse;
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!))
publicKey: Array.from(new Uint8Array(response.getPublicKey()!))
};
} 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.`);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Proton Pass authentication failed: ${errorMessage}. Please ensure Proton Pass is installed and enabled.`);
}
throw error;
@ -229,10 +233,9 @@ export class WebAuthnService {
console.log('🔐 Key encrypted with WebAuthn credential');
return encryptedKey;
} catch (error) {
secureLogger.error('Failed to encrypt key with WebAuthn', {
secureLogger.error('Failed to encrypt key with WebAuthn', error as Error, {
component: 'WebAuthnService',
operation: 'encryptKeyWithWebAuthn',
error: error instanceof Error ? error.message : String(error)
operation: 'encryptKeyWithWebAuthn'
});
throw error;
}
@ -241,31 +244,34 @@ export class WebAuthnService {
/**
* Sauvegarde une clé chiffrée dans IndexedDB
*/
private async saveEncryptedKey(encryptedKey: string, credentialId: string, securityMode: SecurityMode): Promise<void> {
/**
* Sauvegarde la clé PBKDF2 chiffrée dans IndexedDB
* NE PAS stocker credentialId avec la clé chiffrée
*/
private async savePBKDF2Key(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');
console.log(`🔍 Available stores in ${DATABASE_CONFIG.name}:`, Array.from(db.objectStoreNames));
const transaction = db.transaction([DATABASE_CONFIG.stores.pbkdf2keys.name], 'readwrite');
const store = transaction.objectStore(DATABASE_CONFIG.stores.pbkdf2keys.name);
await new Promise<void>((resolve, reject) => {
// Stocker seulement la clé chiffrée (pas de credentialId au même endroit)
// Utiliser le securityMode comme clé d'enregistrement
// NE PAS stocker credentialId avec la clé chiffrée
const request = store.put({
encryptedKey,
securityMode: securityMode, // Mode de sécurité dynamique
encryptedKey, // Clé PBKDF2 chiffrée avec WebAuthn
timestamp: Date.now()
}, 'pbkdf2-key');
}, securityMode);
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');
console.log(`🔐 PBKDF2 key stored with security mode: ${securityMode}`);
} catch (error) {
secureLogger.error('Failed to save encrypted key', {
secureLogger.error('Failed to save PBKDF2 key', error as Error, {
component: 'WebAuthnService',
operation: 'saveEncryptedKey',
error: error instanceof Error ? error.message : String(error)
operation: 'savePBKDF2Key'
});
throw error;
}
@ -273,46 +279,88 @@ export class WebAuthnService {
/**
* Récupère une clé chiffrée avec WebAuthn (récupération dynamique)
* Récupère et déchiffre la clé PBKDF2 avec WebAuthn
*/
async retrieveKeyWithWebAuthn(): Promise<string | null> {
async retrieveKeyWithWebAuthn(securityMode: SecurityMode): Promise<string | null> {
try {
// Récupérer la clé chiffrée depuis IndexedDB
// Récupérer la clé PBKDF2 chiffrée depuis IndexedDB avec le securityMode comme clé
const db = await this.openDatabase();
const transaction = db.transaction(['webauthn-keys'], 'readonly');
const store = transaction.objectStore('webauthn-keys');
const transaction = db.transaction([DATABASE_CONFIG.stores.pbkdf2keys.name], 'readonly');
const store = transaction.objectStore(DATABASE_CONFIG.stores.pbkdf2keys.name);
const result = await new Promise<any>((resolve, reject) => {
const request = store.get('pbkdf2-key');
const request = store.get(securityMode);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
if (!result || !result.encryptedKey) {
console.log('🔍 No encrypted key found in WebAuthnKeysDB');
console.log(`🔍 No PBKDF2 key found for security mode: ${securityMode}`);
return null;
}
// Récupérer credentialId dynamiquement via WebAuthn
// Récupérer le 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();
// Déchiffrer la clé avec le credentialId WebAuthn
const encrypted = atob(result.encryptedKey);
const combined = new Uint8Array(encrypted.length);
for (let i = 0; i < encrypted.length; i++) {
combined[i] = encrypted.charCodeAt(i);
}
const decryptedKey = await encryptionService.decryptWithPassword(result.encryptedKey, credentialId);
console.log('🔐 Key decrypted with WebAuthn credential');
// Extraire salt (16 bytes), iv (12 bytes) et données chiffrées
const salt = combined.slice(0, 16);
const iv = combined.slice(16, 28);
const encryptedData = combined.slice(28);
// Dériver la clé avec PBKDF2
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(credentialId),
'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,
['decrypt']
);
// Déchiffrer
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
cryptoKey,
encryptedData
);
const decryptedKey = new TextDecoder().decode(decrypted);
console.log('🔐 PBKDF2 key decrypted with WebAuthn');
return decryptedKey;
} catch (error) {
secureLogger.error('Failed to retrieve key with WebAuthn', {
secureLogger.error('Failed to retrieve PBKDF2 key with WebAuthn', error as Error, {
component: 'WebAuthnService',
operation: 'retrieveKeyWithWebAuthn',
error: error instanceof Error ? error.message : String(error)
operation: 'retrieveKeyWithWebAuthn'
});
return null;
}
@ -348,10 +396,10 @@ export class WebAuthnService {
*/
private async openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open('WebAuthnKeysDB', 1);
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version);
request.onerror = () => {
secureLogger.error('Failed to open WebAuthn database', new Error('Database open failed'), {
secureLogger.error('Failed to open database', new Error('Database open failed'), {
component: 'WebAuthnService',
operation: 'openDatabase'
});
@ -364,8 +412,12 @@ export class WebAuthnService {
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('webauthn-keys')) {
db.createObjectStore('webauthn-keys');
console.log(`🔄 ${DATABASE_CONFIG.name} database upgrade needed, creating stores...`);
if (!db.objectStoreNames.contains(DATABASE_CONFIG.stores.pbkdf2keys.name)) {
db.createObjectStore(DATABASE_CONFIG.stores.pbkdf2keys.name);
console.log(`${DATABASE_CONFIG.stores.pbkdf2keys.name} store created`);
} else {
console.log(`${DATABASE_CONFIG.stores.pbkdf2keys.name} store already exists`);
}
};
});

View File

@ -0,0 +1,111 @@
/**
* Database Configuration - Configuration centralisée de la base de données IndexedDB
*
* Ce fichier centralise toutes les définitions de la base de données pour éviter
* les erreurs de version et d'incohérence entre les différents fichiers.
*/
export const DATABASE_CONFIG = {
name: '4nk',
version: 3,
stores: {
wallet: {
name: 'wallet',
keyPath: 'pre_id',
indices: []
},
credentials: {
name: 'credentials',
keyPath: null,
indices: []
},
pbkdf2keys: {
name: 'pbkdf2keys',
keyPath: null,
indices: []
},
labels: {
name: 'labels',
keyPath: 'emoji',
indices: []
},
processes: {
name: 'processes',
keyPath: null,
indices: []
},
shared_secrets: {
name: 'shared_secrets',
keyPath: null,
indices: []
},
unconfirmed_secrets: {
name: 'unconfirmed_secrets',
keyPath: null,
autoIncrement: true,
indices: []
},
diffs: {
name: 'diffs',
keyPath: 'value_commitment',
indices: [
{ name: 'byStateId', keyPath: 'state_id', unique: false },
{ name: 'byNeedValidation', keyPath: 'need_validation', unique: false },
{ name: 'byStatus', keyPath: 'validation_status', unique: false }
]
},
data: {
name: 'data',
keyPath: null,
indices: []
}
}
} as const;
/**
* Ouvrir la base de données IndexedDB avec la configuration centralisée
*/
export async function openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version);
request.onerror = () => {
console.error(`❌ Failed to open database ${DATABASE_CONFIG.name}:`, request.error);
reject(request.error);
};
request.onsuccess = () => {
console.log(`✅ Database ${DATABASE_CONFIG.name} opened successfully`);
resolve(request.result);
};
request.onupgradeneeded = () => {
const db = request.result;
console.log(`🔄 Database upgrade needed for ${DATABASE_CONFIG.name} version ${DATABASE_CONFIG.version}`);
// Créer les stores manquants
Object.values(DATABASE_CONFIG.stores).forEach(storeConfig => {
if (!db.objectStoreNames.contains(storeConfig.name)) {
const options: IDBObjectStoreParameters = {};
if (storeConfig.keyPath) {
options.keyPath = storeConfig.keyPath;
}
if ('autoIncrement' in storeConfig && storeConfig.autoIncrement) {
options.autoIncrement = true;
}
const store = db.createObjectStore(storeConfig.name, options);
// Créer les index
storeConfig.indices.forEach(indexConfig => {
store.createIndex(indexConfig.name, indexConfig.keyPath, { unique: indexConfig.unique || false });
});
console.log(`✅ Created store: ${storeConfig.name}`);
} else {
console.log(`✅ Store already exists: ${storeConfig.name}`);
}
});
};
});
}

View File

@ -1,10 +1,11 @@
import Services from './service';
import { DATABASE_CONFIG } from './database-config';
export class Database {
private static instance: Database;
private db: IDBDatabase | null = null;
private dbName: string = '4nk';
private dbVersion: number = 2;
private dbName: string = DATABASE_CONFIG.name;
private dbVersion: number = DATABASE_CONFIG.version;
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
private messageChannel: MessageChannel | null = null;
private messageChannelForGet: MessageChannel | null = null;
@ -49,6 +50,11 @@ export class Database {
options: {},
indices: [],
},
AnkPBKDF2Keys: {
name: 'pbkdf2keys',
options: {},
indices: [],
},
};
// Private constructor to prevent direct instantiation from outside

View File

@ -36,7 +36,7 @@ export class EventBus {
* Ajoute un écouteur d'événement
*/
on(event: string, listener: EventListener): void {
if (this.isDestroyed) return;
if (this.isDestroyed) {return;}
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
@ -60,7 +60,7 @@ export class EventBus {
* Ajoute un écouteur d'événement qui ne se déclenche qu'une fois
*/
once(event: string, listener: EventListener): void {
if (this.isDestroyed) return;
if (this.isDestroyed) {return;}
const onceListener = (data?: EventData) => {
listener(data);
@ -74,10 +74,10 @@ export class EventBus {
* Supprime un écouteur d'événement
*/
off(event: string, listener: EventListener): void {
if (this.isDestroyed) return;
if (this.isDestroyed) {return;}
const eventListeners = this.listeners.get(event);
if (!eventListeners) return;
if (!eventListeners) {return;}
const index = eventListeners.indexOf(listener);
if (index > -1) {
@ -94,10 +94,10 @@ export class EventBus {
* Émet un événement
*/
emit(event: string, data?: EventData): void {
if (this.isDestroyed) return;
if (this.isDestroyed) {return;}
const eventListeners = this.listeners.get(event);
if (!eventListeners || eventListeners.length === 0) return;
if (!eventListeners || eventListeners.length === 0) {return;}
// Créer une copie pour éviter les modifications pendant l'itération
const listeners = [...eventListeners];
@ -115,10 +115,10 @@ export class EventBus {
* Émet un événement de manière asynchrone
*/
async emitAsync(event: string, data?: EventData): Promise<void> {
if (this.isDestroyed) return;
if (this.isDestroyed) {return;}
const eventListeners = this.listeners.get(event);
if (!eventListeners || eventListeners.length === 0) return;
if (!eventListeners || eventListeners.length === 0) {return;}
const listeners = [...eventListeners];
const promises = listeners.map(listener => {
@ -138,7 +138,7 @@ export class EventBus {
* Supprime tous les écouteurs d'un événement ou tous les écouteurs
*/
removeAllListeners(event?: string): void {
if (this.isDestroyed) return;
if (this.isDestroyed) {return;}
if (event) {
this.listeners.delete(event);
@ -191,7 +191,7 @@ export class EventBus {
* Nettoie les écouteurs orphelins
*/
cleanup(): void {
if (this.isDestroyed) return;
if (this.isDestroyed) {return;}
// Supprimer les écouteurs qui ne sont plus dans les souscriptions
this.listeners.forEach((listeners, event) => {

View File

@ -41,7 +41,7 @@ export class MemoryManager {
* Démarre le monitoring de la mémoire
*/
startMonitoring(): void {
if (this.isMonitoring) return;
if (this.isMonitoring) {return;}
this.isMonitoring = true;
this.logMemoryStats();
@ -92,10 +92,10 @@ export class MemoryManager {
*/
getCache<T>(cacheName: string, key: string): T | null {
const cache = this.caches.get(cacheName);
if (!cache) return null;
if (!cache) {return null;}
const entry = cache.get(key);
if (!entry) return null;
if (!entry) {return null;}
// Vérifier l'âge de l'entrée
if (Date.now() - entry.timestamp > this.maxCacheAge) {
@ -115,7 +115,7 @@ export class MemoryManager {
*/
deleteCache(cacheName: string, key: string): boolean {
const cache = this.caches.get(cacheName);
if (!cache) return false;
if (!cache) {return false;}
return cache.delete(key);
}
@ -142,7 +142,7 @@ export class MemoryManager {
*/
getCacheStats(cacheName: string): { size: number; entries: any[] } {
const cache = this.caches.get(cacheName);
if (!cache) return { size: 0, entries: [] };
if (!cache) {return { size: 0, entries: [] };}
const entries = Array.from(cache.entries()).map(([key, entry]) => ({
key,
@ -162,7 +162,7 @@ export class MemoryManager {
*/
getMemoryStats(): MemoryStats | null {
const memory = (performance as any).memory;
if (!memory) return null;
if (!memory) {return null;}
return {
usedJSHeapSize: memory.usedJSHeapSize,
@ -177,7 +177,7 @@ export class MemoryManager {
*/
private checkMemoryUsage(): void {
const stats = this.getMemoryStats();
if (!stats) return;
if (!stats) {return;}
if (stats.usedJSHeapSize > this.memoryThreshold) {
this.performMemoryCleanup();
@ -226,7 +226,7 @@ export class MemoryManager {
*/
private evictLeastUsedEntries(): void {
this.caches.forEach(cache => {
if (cache.size <= this.maxCacheSize) return;
if (cache.size <= this.maxCacheSize) {return;}
// Trier par nombre d'accès et dernière utilisation
const entries = Array.from(cache.entries()).sort((a, b) => {

View File

@ -37,9 +37,9 @@ export default class ModalService {
let html = modalHtml;
html = html.replace('{{device1}}', myAddress);
html = html.replace('{{device2}}', receiverAddress);
if (container) container.innerHTML += html;
if (container) {container.innerHTML += html;}
const modal = document.getElementById('login-modal');
if (modal) modal.style.display = 'flex';
if (modal) {modal.style.display = 'flex';}
const newScript = document.createElement('script');
newScript.setAttribute('type', 'module');
@ -117,7 +117,7 @@ export default class ModalService {
// Modal injection methods removed - using confirmation modal instead
this.modal = document.getElementById('confirmation-modal');
if (this.modal) this.modal.style.display = 'flex';
if (this.modal) {this.modal.style.display = 'flex';}
// Close modal when clicking outside of it
window.onclick = event => {
@ -130,7 +130,7 @@ export default class ModalService {
console.log('=============> Confirm Login');
}
async closeLoginModal() {
if (this.modal) this.modal.style.display = 'none';
if (this.modal) {this.modal.style.display = 'none';}
}
async showConfirmationModal(
@ -190,6 +190,6 @@ export default class ModalService {
async closeConfirmationModal() {
const service = await Services.getInstance();
await service.unpairDevice();
if (this.modal) this.modal.style.display = 'none';
if (this.modal) {this.modal.style.display = 'none';}
}
}

View File

@ -151,7 +151,7 @@ export class PairingService {
async generatePairingWords(): Promise<PairingWords | null> {
try {
const device = await this.deviceRepo.getDevice();
if (!device || !device.sp_wallet?.address) {
if (!device?.sp_wallet?.address) {
throw new Error('No device address found');
}

View File

@ -51,7 +51,7 @@ export class PerformanceMonitor {
* Démarre le monitoring des performances
*/
startMonitoring(): void {
if (this.isMonitoring) return;
if (this.isMonitoring) {return;}
this.isMonitoring = true;
console.log('📊 Performance monitoring started');
@ -73,7 +73,7 @@ export class PerformanceMonitor {
recordMetric(name: string, value: number, unit: string = 'ms'): void {
// Use unit parameter
console.log('Performance metric unit:', unit);
if (!this.isMonitoring) return;
if (!this.isMonitoring) {return;}
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
@ -130,7 +130,7 @@ export class PerformanceMonitor {
*/
getMetricStats(name: string): PerformanceStats | null {
const values = this.metrics.get(name);
if (!values || values.length === 0) return null;
if (!values || values.length === 0) {return null;}
const sorted = [...values].sort((a, b) => a - b);
const sum = values.reduce((acc, val) => acc + val, 0);
@ -173,7 +173,7 @@ export class PerformanceMonitor {
// Analyser chaque métrique
Object.entries(metrics).forEach(([name, stats]) => {
const threshold = this.thresholds[name];
if (!threshold) return;
if (!threshold) {return;}
const percentage = (stats.average / threshold.critical) * 100;
healthScore -= Math.max(0, 100 - percentage);
@ -217,7 +217,7 @@ export class PerformanceMonitor {
*/
private checkThresholds(name: string, value: number): void {
const threshold = this.thresholds[name];
if (!threshold) return;
if (!threshold) {return;}
if (value > threshold.critical) {
console.warn(`🚨 Critical performance threshold exceeded for ${name}: ${value}ms`);
@ -230,7 +230,7 @@ export class PerformanceMonitor {
* Configure les observateurs de performance
*/
private setupPerformanceObservers(): void {
if (!window.PerformanceObserver) return;
if (!window.PerformanceObserver) {return;}
// Observer les mesures de performance
const measureObserver = new PerformanceObserver((list) => {

View File

@ -110,8 +110,8 @@ export class SecureCredentialsService {
switch (securityMode) {
case 'proton-pass':
case 'os':
// Récupérer la clé chiffrée avec WebAuthn (récupération dynamique)
return await webAuthnService.retrieveKeyWithWebAuthn();
// Récupérer la clé chiffrée avec WebAuthn
return await webAuthnService.retrieveKeyWithWebAuthn(securityMode);
case 'browser':
// Récupérer la clé du gestionnaire de mots de passe

View File

@ -65,7 +65,7 @@ export class SecureKeyManager {
async getPrivateKey(password: string): Promise<string | null> {
try {
const encryptedData = await this.getEncryptedData();
if (!encryptedData) return null;
if (!encryptedData) {return null;}
const derivedKey = await this.deriveKey(password);
const iv = encryptedData.slice(0, 12);

View File

@ -91,7 +91,7 @@ export class SecureLogger {
* Sanitise le contexte pour supprimer les données sensibles
*/
private sanitizeContext(context?: LogContext): LogContext | undefined {
if (!context) return undefined;
if (!context) {return undefined;}
const sanitized: LogContext = {};
const sensitiveKeys = [

View File

@ -1,10 +1,10 @@
/**
* 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
* NOTE: Le mode de sécurité est stocké uniquement avec la clé PBKDF2, pas dans un store séparé
*/
import { secureLogger } from './secure-logger';
import Database from './database.service';
export type SecurityMode = 'proton-pass' | 'os' | 'browser' | 'otp' | '2fa' | 'password' | 'none';
@ -25,11 +25,10 @@ export interface SecurityModeConfig {
export class SecurityModeService {
private static instance: SecurityModeService;
private database: Database;
private currentMode: SecurityMode | null = null;
private constructor() {
this.database = new Database();
// N'instancier pas Database ici, il sera récupéré via getInstance() quand nécessaire
}
public static getInstance(): SecurityModeService {
@ -41,78 +40,32 @@ export class SecurityModeService {
/**
* Récupère le mode de sécurisation actuel
* NOTE: Le mode est stocké uniquement en mémoire, dérivé de la clé PBKDF2 stockée
*/
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', {
// Le mode de sécurité n'est pas stocké en base, il est dérivé de la clé PBKDF2
// Ici on retourne null si aucun mode n'est en mémoire
// Le mode sera défini lors de la génération de la clé PBKDF2
secureLogger.info('No security mode in memory (will be set when PBKDF2 key is generated)', {
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
* NOTE: Le mode de sécurité est stocké uniquement avec la clé PBKDF2, pas dans un store séparé
*/
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
});
// Stocker uniquement en mémoire
this.currentMode = mode;
secureLogger.info('Security mode set successfully', {
@ -133,13 +86,8 @@ export class 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
});
// NE PAS FAIRE DE FALLBACK - échouer complètement
throw error;
}
}
@ -286,7 +234,7 @@ export class SecurityModeService {
*/
public async isCurrentModeSecure(): Promise<boolean> {
const currentMode = await this.getCurrentMode();
if (!currentMode) return false;
if (!currentMode) {return false;}
const config = this.getSecurityModeConfig(currentMode);
return config.securityLevel === 'high' || config.securityLevel === 'medium';
@ -350,7 +298,8 @@ export class SecurityModeService {
*/
public async resetSecurityMode(): Promise<void> {
try {
await this.database.deleteObject('security_settings', 'current_mode');
const database = await Database.getInstance();
await database.deleteObject('security_settings', 'current_mode');
this.currentMode = null;
secureLogger.info('Security mode reset', {
@ -371,7 +320,8 @@ export class SecurityModeService {
*/
public async getSecurityModeHistory(): Promise<any[]> {
try {
const history = await this.database.getObject('security_settings', 'mode_history') || [];
const database = await Database.getInstance();
const history = await database.getObject('security_settings', 'mode_history') || [];
return history;
} catch (error) {
secureLogger.error('Failed to retrieve security mode history', error as Error, {
@ -399,7 +349,12 @@ export class SecurityModeService {
history.splice(10);
}
await this.database.setObject('security_settings', 'mode_history', history);
const database = await Database.getInstance();
await database.addObject({
storeName: 'security_settings',
key: 'mode_history',
object: history
});
} catch (error) {
secureLogger.error('Failed to add to security mode history', error as Error, {
component: 'SecurityModeService',

View File

@ -448,7 +448,7 @@ export default class Services {
// Use processId parameter
console.log('Getting cached process:', processId);
const process = this.processesCache[processId];
if (!process) return null;
if (!process) {return null;}
const now = Date.now();
if (now - (process as any).timestamp > this.cacheExpiry) {
@ -704,7 +704,7 @@ export default class Services {
if (!roles) {
throw new Error('No roles found');
}
let members: Set<Member> = new Set();
const members: Set<Member> = new Set();
for (const role of Object.values(roles!)) {
for (const member of role.members) {
// Check if we know the member that matches this id
@ -726,7 +726,7 @@ export default class Services {
}
// If pairedAddresses is not in the current state, look in previous states
if (!publicData || !publicData['pairedAddresses']) {
if (!publicData?.['pairedAddresses']) {
// Look for pairedAddresses in previous states
for (let i = process.states.length - 1; i >= 0; i--) {
const state = process.states[i];
@ -737,7 +737,7 @@ export default class Services {
}
}
if (!publicData || !publicData['pairedAddresses']) {
if (!publicData?.['pairedAddresses']) {
throw new Error('Not a pairing process');
}
const decodedAddresses = this.decodeValue(publicData['pairedAddresses']);
@ -749,14 +749,14 @@ export default class Services {
// Ensure the amount is available before proceeding
await this.getTokensFromFaucet();
let unconnectedAddresses = new Set<string>();
const unconnectedAddresses = new Set<string>();
const myAddress = this.getDeviceAddress();
for (const member of Array.from(members)) {
const sp_addresses = member.sp_addresses;
if (!sp_addresses || sp_addresses.length === 0) continue;
if (!sp_addresses || sp_addresses.length === 0) {continue;}
for (const address of sp_addresses) {
// For now, we ignore our own device address, although there might be use cases for having a secret with ourselves
if (address === myAddress) continue;
if (address === myAddress) {continue;}
if ((await this.getSecretForAddress(address)) === null) {
unconnectedAddresses.add(address);
}
@ -1271,7 +1271,7 @@ export default class Services {
try {
// Get current device to check birthday
const device = await this.getDeviceFromDatabase();
if (device && device.sp_wallet) {
if (device?.sp_wallet) {
console.log('🔍 Device wallet state:', {
birthday: device.sp_wallet.birthday,
last_scan: device.sp_wallet.last_scan,
@ -1332,7 +1332,7 @@ export default class Services {
// Check wallet state in SDK
try {
const device = await this.getDeviceFromDatabase();
if (device && device.sp_wallet) {
if (device?.sp_wallet) {
console.log('🔍 Wallet state:');
console.log('- Last scan:', device.sp_wallet.last_scan);
console.log('- Current block:', this.currentBlockHeight);
@ -1719,7 +1719,7 @@ export default class Services {
private ensureWalletKeysAvailable(): void {
try {
const device = this.dumpDeviceFromMemory();
if (!device || !device.sp_wallet) {
if (!device?.sp_wallet) {
throw new Error('❌ Wallet not initialized - WebAuthn decryption required');
}
@ -1749,7 +1749,7 @@ export default class Services {
// Additional debugging: check wallet state
try {
const device = this.dumpDeviceFromMemory();
if (device && device.sp_wallet) {
if (device?.sp_wallet) {
console.log(`🔍 Wallet debugging info:`, {
birthday: device.sp_wallet.birthday,
last_scan: device.sp_wallet.last_scan,
@ -2021,7 +2021,7 @@ export default class Services {
try {
const device = await this.getDeviceFromDatabase();
if (!device || !device.sp_wallet) {
if (!device?.sp_wallet) {
throw new Error('Device not found or wallet not initialized');
}
@ -2600,14 +2600,14 @@ export default class Services {
const existing = await this.getProcess(processId);
if (existing) {
// Look for state id we don't know yet
let newStates: string[] = [];
let newRoles: Record<string, RoleDefinition>[] = [];
const newStates: string[] = [];
const newRoles: Record<string, RoleDefinition>[] = [];
if (!Array.isArray(process.states)) {
console.warn('process.states is not an array:', process.states);
continue;
}
for (const state of process.states) {
if (!state || !state.state_id) {
if (!state?.state_id) {
continue;
} // shouldn't happen
if (state.state_id === EMPTY32BYTES) {
@ -2944,7 +2944,7 @@ export default class Services {
}
public getLastCommitedState(process: Process): ProcessState | null {
if (process.states.length === 0) return null;
if (process.states.length === 0) {return null;}
const processTip = process.states[process.states.length - 1].commited_in;
const lastCommitedState = process.states.findLast(state => state.commited_in !== processTip);
if (lastCommitedState) {
@ -2955,7 +2955,7 @@ export default class Services {
}
public getLastCommitedStateIndex(process: Process): number | null {
if (process.states.length === 0) return null;
if (process.states.length === 0) {return null;}
const processTip = process.states[process.states.length - 1].commited_in;
for (let i = process.states.length - 1; i >= 0; i--) {
if (process.states[i].commited_in !== processTip) {
@ -2966,14 +2966,14 @@ export default class Services {
}
public getUncommitedStates(process: Process): ProcessState[] {
if (process.states.length === 0) return [];
if (process.states.length === 0) {return [];}
const processTip = process.states[process.states.length - 1].commited_in;
const res = process.states.filter(state => state.commited_in === processTip);
return res.filter(state => state.state_id !== EMPTY32BYTES);
}
public getStateFromId(process: Process, stateId: string): ProcessState | null {
if (process.states.length === 0) return null;
if (process.states.length === 0) {return null;}
const state = process.states.find(state => state.state_id === stateId);
if (state) {
return state;
@ -2983,7 +2983,7 @@ export default class Services {
}
public getNextStateAfterId(process: Process, stateId: string): ProcessState | null {
if (process.states.length === 0) return null;
if (process.states.length === 0) {return null;}
const index = process.states.findIndex(state => state.state_id === stateId);

View File

@ -182,7 +182,7 @@ export class WebSocketManager {
* Configure les écouteurs d'événements WebSocket
*/
private setupEventListeners(resolve: Function, _reject: Function): void {
if (!this.ws) return;
if (!this.ws) {return;}
this.ws.onopen = () => {
this.status.isConnected = true;

View File

@ -63,7 +63,7 @@ class Logger {
}
private log(level: LogLevel, message: string, context?: LogContext): void {
if (!this.shouldLog(level)) return;
if (!this.shouldLog(level)) {return;}
const entry = this.createLogEntry(level, message, context);
this.logs.push(entry);

View File

@ -2344,11 +2344,11 @@ function showLoadingState() {
// Update loading message
const loadingText = loadingFlow.querySelector('h2');
const loadingDesc = loadingFlow.querySelector('p');
if (loadingText) loadingText.textContent = 'Initializing...';
if (loadingDesc) loadingDesc.textContent = 'Setting up secure pairing interface';
if (loadingText) {loadingText.textContent = 'Initializing...';}
if (loadingDesc) {loadingDesc.textContent = 'Setting up secure pairing interface';}
}
if (creatorFlow) (creatorFlow as HTMLElement).style.display = 'none';
if (joinerFlow) (joinerFlow as HTMLElement).style.display = 'none';
if (creatorFlow) {(creatorFlow as HTMLElement).style.display = 'none';}
if (joinerFlow) {(joinerFlow as HTMLElement).style.display = 'none';}
}
// Detect flow and show appropriate interface
@ -2364,16 +2364,16 @@ async function detectAndShowFlow() {
if (hasWords) {
// Joiner flow
if (loadingFlow) (loadingFlow as HTMLElement).style.display = 'none';
if (creatorFlow) (creatorFlow as HTMLElement).style.display = 'none';
if (joinerFlow) (joinerFlow as HTMLElement).style.display = 'block';
if (loadingFlow) {(loadingFlow as HTMLElement).style.display = 'none';}
if (creatorFlow) {(creatorFlow as HTMLElement).style.display = 'none';}
if (joinerFlow) {(joinerFlow as HTMLElement).style.display = 'block';}
updateJoinerStatus('Ready to join pairing');
} else {
// Creator flow
if (loadingFlow) (loadingFlow as HTMLElement).style.display = 'none';
if (creatorFlow) (creatorFlow as HTMLElement).style.display = 'block';
if (joinerFlow) (joinerFlow as HTMLElement).style.display = 'none';
if (loadingFlow) {(loadingFlow as HTMLElement).style.display = 'none';}
if (creatorFlow) {(creatorFlow as HTMLElement).style.display = 'block';}
if (joinerFlow) {(joinerFlow as HTMLElement).style.display = 'none';}
updateCreatorStatus('Ready to create pairing');
}

View File

@ -21,7 +21,7 @@ export function addSubscription(
event: any,
eventHandler: EventListenerOrEventListenerObject
): void {
if (!element) return;
if (!element) {return;}
subscriptions.push({ element, event, eventHandler });
element.addEventListener(event, eventHandler);
}

View File

@ -4,7 +4,7 @@ import { messageValidator } from './services/message-validator';
import { secureLogger } from './services/secure-logger';
let ws: WebSocket;
let messageQueue: string[] = [];
const messageQueue: string[] = [];
export async function initWebsocket(url: string) {
ws = new WebSocket(url);