This commit is contained in:
root
2025-11-25 09:56:15 +03:00
commit 68c8f0e80d
23717 changed files with 3200521 additions and 0 deletions

View 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;