init
This commit is contained in:
261
mc_test/src/renderer/services/EncryptionService.ts
Executable file
261
mc_test/src/renderer/services/EncryptionService.ts
Executable file
@ -0,0 +1,261 @@
|
||||
import { log } from '../utils/logger';
|
||||
|
||||
/**
|
||||
* Client-side password encryption service
|
||||
* Uses Web Crypto API with AES-256-GCM algorithm
|
||||
*/
|
||||
class EncryptionService {
|
||||
private encryptionKey: CryptoKey | null = null;
|
||||
|
||||
/**
|
||||
* Initialize encryption key
|
||||
* @param keyBase64 Base64 encoded key from server
|
||||
*/
|
||||
async initializeKey(keyBase64: string): Promise<void> {
|
||||
try {
|
||||
// this._keyBase64 = keyBase64;
|
||||
|
||||
// Decode Base64 key
|
||||
const keyBuffer = this.base64ToArrayBuffer(keyBase64);
|
||||
|
||||
// Import key for use with Web Crypto API
|
||||
this.encryptionKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyBuffer,
|
||||
{ name: 'AES-GCM' },
|
||||
false, // not exportable
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
log.info('encryption-service', 'Encryption key initialized successfully');
|
||||
} catch (error) {
|
||||
log.error('encryption-service', 'Failed to initialize encryption key', { error });
|
||||
throw new Error('Failed to initialize encryption key');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt password using AES-GCM and AEAD
|
||||
* @param password Password in plain text
|
||||
* @param sessionId Session ID for AAD
|
||||
* @returns Encrypted password in Base64
|
||||
*/
|
||||
async encryptPassword(password: string, sessionId?: string): Promise<{encrypted: string, nonce: string, timestamp: number}> {
|
||||
if (!this.encryptionKey) {
|
||||
throw new Error('Encryption key not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
// CRITICAL: Generate unique IV (12 bytes for GCM)
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
|
||||
// Convert password to ArrayBuffer
|
||||
const passwordBuffer = new TextEncoder().encode(password);
|
||||
|
||||
// AAD used ONLY for saved machines (when sessionId exists)
|
||||
// For temporary connection passwords AAD is NOT needed
|
||||
const encryptParams: AesGcmParams = {
|
||||
name: 'AES-GCM',
|
||||
iv: iv
|
||||
};
|
||||
|
||||
let timestamp = 0;
|
||||
let clientNonce: Uint8Array | undefined;
|
||||
|
||||
// If sessionId provided - use AAD for additional security
|
||||
if (sessionId) {
|
||||
timestamp = Date.now();
|
||||
clientNonce = crypto.getRandomValues(new Uint8Array(16));
|
||||
const aadData = {
|
||||
sessionId: sessionId,
|
||||
timestamp: timestamp,
|
||||
clientNonce: Array.from(clientNonce)
|
||||
};
|
||||
encryptParams.additionalData = new TextEncoder().encode(JSON.stringify(aadData));
|
||||
}
|
||||
|
||||
// Encrypt
|
||||
const encryptedBuffer = await crypto.subtle.encrypt(
|
||||
encryptParams,
|
||||
this.encryptionKey,
|
||||
passwordBuffer
|
||||
);
|
||||
|
||||
// Combine IV + encrypted data
|
||||
const combined = new Uint8Array(iv.length + encryptedBuffer.byteLength);
|
||||
combined.set(iv, 0);
|
||||
combined.set(new Uint8Array(encryptedBuffer), iv.length);
|
||||
|
||||
// Convert to Base64
|
||||
const encrypted = this.arrayBufferToBase64(combined.buffer);
|
||||
const nonce = clientNonce ? this.arrayBufferToBase64(clientNonce.buffer as ArrayBuffer) : '';
|
||||
|
||||
log.debug('encryption-service', 'Password encrypted successfully', {
|
||||
passwordLength: password.length,
|
||||
encryptedLength: encrypted.length,
|
||||
sessionId: sessionId,
|
||||
usesAAD: !!sessionId
|
||||
});
|
||||
|
||||
return {
|
||||
encrypted,
|
||||
nonce,
|
||||
timestamp
|
||||
};
|
||||
} catch (error) {
|
||||
log.error('encryption-service', 'Failed to encrypt password', { error });
|
||||
throw new Error('Password encryption failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt password (for testing)
|
||||
* @param encryptedPassword Base64 encrypted string
|
||||
* @param sessionId Session ID for AAD (optional - for testing)
|
||||
* @param nonce Client nonce for AAD (optional - for testing)
|
||||
* @param timestamp Timestamp for AAD (optional - for testing)
|
||||
* @returns Password in plain text
|
||||
*/
|
||||
async decryptPassword(
|
||||
encryptedPassword: string,
|
||||
sessionId?: string,
|
||||
nonce?: string,
|
||||
timestamp?: number
|
||||
): Promise<string> {
|
||||
if (!this.encryptionKey) {
|
||||
throw new Error('Encryption key not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
// Decode Base64
|
||||
const combined = this.base64ToArrayBuffer(encryptedPassword);
|
||||
|
||||
// Extract IV (first 12 bytes) and encrypted data
|
||||
const iv = combined.slice(0, 12);
|
||||
const encryptedData = combined.slice(12);
|
||||
|
||||
// FIXED: Create AAD if parameters provided
|
||||
let additionalData: Uint8Array | undefined = undefined;
|
||||
if (sessionId && nonce && timestamp) {
|
||||
const clientNonceArray = new Uint8Array(this.base64ToArrayBuffer(nonce));
|
||||
const aadData = {
|
||||
sessionId: sessionId,
|
||||
timestamp: timestamp,
|
||||
clientNonce: Array.from(clientNonceArray)
|
||||
};
|
||||
additionalData = new TextEncoder().encode(JSON.stringify(aadData));
|
||||
}
|
||||
|
||||
// Decrypt
|
||||
const decryptedBuffer = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: iv,
|
||||
...(additionalData && { additionalData }) // Add AAD if exists
|
||||
},
|
||||
this.encryptionKey,
|
||||
encryptedData
|
||||
);
|
||||
|
||||
// Convert back to string
|
||||
const password = new TextDecoder().decode(decryptedBuffer);
|
||||
|
||||
log.debug('encryption-service', 'Password decrypted successfully');
|
||||
|
||||
return password;
|
||||
} catch (error) {
|
||||
log.error('encryption-service', 'Failed to decrypt password', { error });
|
||||
throw new Error('Password decryption failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if encryption key is initialized
|
||||
*/
|
||||
isInitialized(): boolean {
|
||||
return this.encryptionKey !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current encryption status
|
||||
*/
|
||||
getStatus(): {
|
||||
initialized: boolean;
|
||||
} {
|
||||
return {
|
||||
initialized: this.isInitialized(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear encryption key
|
||||
*/
|
||||
clearKey(): void {
|
||||
this.encryptionKey = null;
|
||||
log.info('encryption-service', 'Encryption key cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Base64 string to ArrayBuffer
|
||||
*/
|
||||
private base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||
const binaryString = atob(base64);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ArrayBuffer to Base64 string
|
||||
*/
|
||||
private arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test encryption/decryption
|
||||
*/
|
||||
async testEncryption(testPassword: string = 'test123', sessionId: string = 'test-session'): Promise<boolean> {
|
||||
try {
|
||||
if (!this.isInitialized()) {
|
||||
throw new Error('Encryption key not initialized');
|
||||
}
|
||||
|
||||
// Encrypt test password
|
||||
const encryptionResult = await this.encryptPassword(testPassword, sessionId);
|
||||
|
||||
// FIXED: Decrypt with same AAD parameters
|
||||
const decrypted = await this.decryptPassword(
|
||||
encryptionResult.encrypted,
|
||||
sessionId,
|
||||
encryptionResult.nonce,
|
||||
encryptionResult.timestamp
|
||||
);
|
||||
|
||||
// Check that we got the same password
|
||||
const success = decrypted === testPassword;
|
||||
|
||||
log.info('encryption-service', 'Encryption test completed', {
|
||||
success,
|
||||
testPasswordLength: testPassword.length,
|
||||
encryptedLength: encryptionResult.encrypted.length
|
||||
});
|
||||
|
||||
return success;
|
||||
} catch (error) {
|
||||
log.error('encryption-service', 'Encryption test failed', { error });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const encryptionService = new EncryptionService();
|
||||
export default encryptionService;
|
||||
Reference in New Issue
Block a user