feat(security): switch master key to non-extractable CryptoKey handle and remove direct access
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled

This commit is contained in:
lockbitchat
2025-10-30 15:16:36 -04:00
parent 77ed4b3e4f
commit 4583db39a2
8 changed files with 84 additions and 199 deletions
+33 -79
View File
@@ -2507,12 +2507,6 @@ var EnhancedSecureCryptoUtils = class _EnhancedSecureCryptoUtils {
encoder.encode(payloadStr) encoder.encode(payloadStr)
); );
payload.mac = Array.from(new Uint8Array(mac)); payload.mac = Array.from(new Uint8Array(mac));
_EnhancedSecureCryptoUtils.secureLog.log("info", "Message encrypted with metadata protection", {
messageId,
sequenceNumber,
hasMetadataProtection: true,
hasPadding: true
});
return payload; return payload;
} catch (error) { } catch (error) {
_EnhancedSecureCryptoUtils.secureLog.log("error", "Message encryption failed", { _EnhancedSecureCryptoUtils.secureLog.log("error", "Message encryption failed", {
@@ -13781,11 +13775,10 @@ var SecureKeyStorage = class {
/** /**
* Get master key (with automatic unlock if needed) * Get master key (with automatic unlock if needed)
*/ */
async _getMasterKey() { async _ensureMasterKeyUnlocked() {
if (!this._masterKeyManager.isUnlocked()) { if (!this._masterKeyManager.isUnlocked()) {
await this._masterKeyManager.unlock(); await this._masterKeyManager.unlock();
} }
return this._masterKeyManager.getMasterKey();
} }
async storeKey(keyId, cryptoKey, metadata = {}) { async storeKey(keyId, cryptoKey, metadata = {}) {
if (!(cryptoKey instanceof CryptoKey)) { if (!(cryptoKey instanceof CryptoKey)) {
@@ -13854,27 +13847,18 @@ var SecureKeyStorage = class {
const dataToEncrypt = typeof keyData === "object" ? JSON.stringify(keyData) : keyData; const dataToEncrypt = typeof keyData === "object" ? JSON.stringify(keyData) : keyData;
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const data = encoder.encode(dataToEncrypt); const data = encoder.encode(dataToEncrypt);
const iv = crypto.getRandomValues(new Uint8Array(12)); await this._ensureMasterKeyUnlocked();
const masterKey = await this._getMasterKey(); const { encryptedData, iv } = await this._masterKeyManager.encryptBytes(data);
const encryptedData = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
masterKey,
data
);
const result = new Uint8Array(iv.length + encryptedData.byteLength); const result = new Uint8Array(iv.length + encryptedData.byteLength);
result.set(iv, 0); result.set(iv, 0);
result.set(new Uint8Array(encryptedData), iv.length); result.set(encryptedData, iv.length);
return result; return result;
} }
async _decryptKeyData(encryptedData) { async _decryptKeyData(encryptedData) {
const iv = encryptedData.slice(0, 12); const iv = encryptedData.slice(0, 12);
const data = encryptedData.slice(12); const data = encryptedData.slice(12);
const masterKey = await this._getMasterKey(); await this._ensureMasterKeyUnlocked();
const decryptedData = await crypto.subtle.decrypt( const decryptedData = await this._masterKeyManager.decryptBytes(data, iv);
{ name: "AES-GCM", iv },
masterKey,
data
);
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const jsonString = decoder.decode(decryptedData); const jsonString = decoder.decode(decryptedData);
try { try {
@@ -14418,8 +14402,7 @@ var SecurePersistentKeyStorage = class {
try { try {
await this._ensureDBInitialized(); await this._ensureDBInitialized();
const jwkData = await crypto.subtle.exportKey("jwk", cryptoKey); const jwkData = await crypto.subtle.exportKey("jwk", cryptoKey);
const masterKey = this._masterKeyManager.getMasterKey(); const { encryptedData, iv } = await this._encryptKeyData(jwkData);
const { encryptedData, iv } = await this._encryptKeyData(jwkData, masterKey);
await this._indexedDB.storeEncryptedKey( await this._indexedDB.storeEncryptedKey(
keyId, keyId,
encryptedData, encryptedData,
@@ -14449,8 +14432,7 @@ var SecurePersistentKeyStorage = class {
if (!keyRecord) { if (!keyRecord) {
return null; return null;
} }
const masterKey = this._masterKeyManager.getMasterKey(); const jwkData = await this._decryptKeyData(keyRecord.encryptedData, keyRecord.iv);
const jwkData = await this._decryptKeyData(keyRecord.encryptedData, keyRecord.iv, masterKey);
const restoredKey = await this._importAsNonExtractable(jwkData, keyRecord.algorithm, keyRecord.usages); const restoredKey = await this._importAsNonExtractable(jwkData, keyRecord.algorithm, keyRecord.usages);
this._keyReferences.set(keyId, restoredKey); this._keyReferences.set(keyId, restoredKey);
await this._indexedDB.updateKeyMetadata(keyId, { lastAccessed: Date.now() }); await this._indexedDB.updateKeyMetadata(keyId, { lastAccessed: Date.now() });
@@ -14499,29 +14481,18 @@ var SecurePersistentKeyStorage = class {
/** /**
* Encrypt key data using master key * Encrypt key data using master key
*/ */
async _encryptKeyData(jwkData, masterKey) { async _encryptKeyData(jwkData) {
const jsonString = JSON.stringify(jwkData); const jsonString = JSON.stringify(jwkData);
const data = new TextEncoder().encode(jsonString); const data = new TextEncoder().encode(jsonString);
const iv = crypto.getRandomValues(new Uint8Array(12)); await this._ensureMasterKeyUnlocked();
const encryptedData = await crypto.subtle.encrypt( return await this._masterKeyManager.encryptBytes(data);
{ name: "AES-GCM", iv },
masterKey,
data
);
return {
encryptedData: new Uint8Array(encryptedData),
iv
};
} }
/** /**
* Decrypt key data using master key * Decrypt key data using master key
*/ */
async _decryptKeyData(encryptedData, iv, masterKey) { async _decryptKeyData(encryptedData, iv) {
const decryptedData = await crypto.subtle.decrypt( await this._ensureMasterKeyUnlocked();
{ name: "AES-GCM", iv }, const decryptedData = await this._masterKeyManager.decryptBytes(encryptedData, iv);
masterKey,
encryptedData
);
const jsonString = new TextDecoder().decode(decryptedData); const jsonString = new TextDecoder().decode(decryptedData);
return JSON.parse(jsonString); return JSON.parse(jsonString);
} }
@@ -14564,7 +14535,7 @@ var SecurePersistentKeyStorage = class {
}; };
var SecureMasterKeyManager = class { var SecureMasterKeyManager = class {
constructor(indexedDBWrapper = null) { constructor(indexedDBWrapper = null) {
this._masterKey = null; this._keyHandle = null;
this._isUnlocked = false; this._isUnlocked = false;
this._sessionTimeout = null; this._sessionTimeout = null;
this._lastActivity = null; this._lastActivity = null;
@@ -14773,7 +14744,7 @@ var SecureMasterKeyManager = class {
password = await this._requestPassword(false); password = await this._requestPassword(false);
} }
const salt = await this._getOrCreateSalt(); const salt = await this._getOrCreateSalt();
this._masterKey = await this._deriveKeyFromPassword(password, salt); this._keyHandle = await this._deriveKeyFromPassword(password, salt);
this._isUnlocked = true; this._isUnlocked = true;
this._lastActivity = Date.now(); this._lastActivity = Date.now();
this._startSessionTimer(); this._startSessionTimer();
@@ -14796,12 +14767,23 @@ var SecureMasterKeyManager = class {
/** /**
* Get master key (only if unlocked) * Get master key (only if unlocked)
*/ */
getMasterKey() { // Prevent direct key access; provide operations only
if (!this._isUnlocked || !this._masterKey) { async encryptBytes(plainBytes) {
if (!this._isUnlocked || !this._keyHandle) {
throw new Error("Master key is locked"); throw new Error("Master key is locked");
} }
this._updateActivity(); this._updateActivity();
return this._masterKey; const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, this._keyHandle, plainBytes);
return { encryptedData: new Uint8Array(encrypted), iv };
}
async decryptBytes(encryptedBytes, iv) {
if (!this._isUnlocked || !this._keyHandle) {
throw new Error("Master key is locked");
}
this._updateActivity();
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, this._keyHandle, encryptedBytes);
return new Uint8Array(decrypted);
} }
/** /**
* Check if master key is unlocked * Check if master key is unlocked
@@ -14824,8 +14806,8 @@ var SecureMasterKeyManager = class {
* Securely wipe master key from memory * Securely wipe master key from memory
*/ */
_secureWipeMasterKey() { _secureWipeMasterKey() {
if (this._masterKey) { if (this._keyHandle) {
this._masterKey = null; this._keyHandle = null;
} }
this._clearTimers(); this._clearTimers();
} }
@@ -14896,10 +14878,8 @@ var EnhancedMinimalHeader = ({
} else if (window.DEBUG_MODE) { } else if (window.DEBUG_MODE) {
} }
} else { } else {
console.warn(" Security calculation returned invalid data");
} }
} catch (error) { } catch (error) {
console.error(" Error in real security calculation:", error);
} finally { } finally {
isUpdating = false; isUpdating = false;
} }
@@ -14940,10 +14920,8 @@ var EnhancedMinimalHeader = ({
if (securityData && securityData.isRealData !== false) { if (securityData && securityData.isRealData !== false) {
setRealSecurityLevel(securityData); setRealSecurityLevel(securityData);
setLastSecurityUpdate(Date.now()); setLastSecurityUpdate(Date.now());
console.log("\u2705 Header security level force-updated");
} }
}).catch((error) => { }).catch((error) => {
console.error("\u274C Force update failed:", error);
}); });
} else { } else {
setLastSecurityUpdate(0); setLastSecurityUpdate(0);
@@ -14971,9 +14949,6 @@ var EnhancedMinimalHeader = ({
setSessionType("premium"); setSessionType("premium");
}; };
const handleConnectionCleaned = () => { const handleConnectionCleaned = () => {
if (window.DEBUG_MODE) {
console.log("\u{1F9F9} Connection cleaned - clearing security data in header");
}
setRealSecurityLevel(null); setRealSecurityLevel(null);
setLastSecurityUpdate(0); setLastSecurityUpdate(0);
setHasActiveSession(false); setHasActiveSession(false);
@@ -14981,9 +14956,6 @@ var EnhancedMinimalHeader = ({
setSessionType("unknown"); setSessionType("unknown");
}; };
const handlePeerDisconnect = () => { const handlePeerDisconnect = () => {
if (window.DEBUG_MODE) {
console.log("\u{1F44B} Peer disconnect detected - clearing security data in header");
}
setRealSecurityLevel(null); setRealSecurityLevel(null);
setLastSecurityUpdate(0); setLastSecurityUpdate(0);
}; };
@@ -15018,15 +14990,9 @@ var EnhancedMinimalHeader = ({
if (webrtcManager && window.EnhancedSecureCryptoUtils) { if (webrtcManager && window.EnhancedSecureCryptoUtils) {
try { try {
realTestResults = await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(webrtcManager); realTestResults = await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(webrtcManager);
console.log("\u2705 Real security tests completed:", realTestResults);
} catch (error) { } catch (error) {
console.error("\u274C Real security tests failed:", error);
} }
} else { } else {
console.log("\u26A0\uFE0F Cannot run security tests:", {
webrtcManager: !!webrtcManager,
cryptoUtils: !!window.EnhancedSecureCryptoUtils
});
} }
if (!realTestResults && !realSecurityLevel) { if (!realTestResults && !realSecurityLevel) {
alert("Security verification in progress...\nPlease wait for real-time cryptographic verification to complete."); alert("Security verification in progress...\nPlease wait for real-time cryptographic verification to complete.");
@@ -15045,7 +15011,6 @@ var EnhancedMinimalHeader = ({
passedChecks: 0, passedChecks: 0,
totalChecks: 0 totalChecks: 0
}; };
console.log("Using fallback security data:", securityData);
} }
let message = `REAL-TIME SECURITY VERIFICATION let message = `REAL-TIME SECURITY VERIFICATION
@@ -15261,18 +15226,7 @@ Right-click or Ctrl+click to disconnect`,
}; };
const securityDetails = getSecurityIndicatorDetails(); const securityDetails = getSecurityIndicatorDetails();
React.useEffect(() => { React.useEffect(() => {
window.debugHeaderSecurity = () => { window.debugHeaderSecurity = void 0;
console.log("\u{1F50D} Header Security Debug:", {
realSecurityLevel,
lastSecurityUpdate,
isConnected,
webrtcManagerProp: !!webrtcManager,
windowWebrtcManager: !!window.webrtcManager,
cryptoUtils: !!window.EnhancedSecureCryptoUtils,
displaySecurityLevel,
securityDetails
});
};
return () => { return () => {
delete window.debugHeaderSecurity; delete window.debugHeaderSecurity;
}; };
+2 -2
View File
File diff suppressed because one or more lines are too long
Vendored
-11
View File
@@ -2266,7 +2266,6 @@ var EnhancedSecureP2PChat = () => {
return Promise.resolve(false); return Promise.resolve(false);
} }
} }
console.log("Processing single QR code:", scannedData.substring(0, 50) + "...");
let parsedData; let parsedData;
if (typeof window.decodeAnyPayload === "function") { if (typeof window.decodeAnyPayload === "function") {
const any = window.decodeAnyPayload(scannedData); const any = window.decodeAnyPayload(scannedData);
@@ -2568,24 +2567,14 @@ var EnhancedSecureP2PChat = () => {
}]); }]);
let offer; let offer;
try { try {
console.log("DEBUG: Processing offer input:", offerInput.trim().substring(0, 100) + "...");
console.log("DEBUG: decodeAnyPayload available:", typeof window.decodeAnyPayload === "function");
console.log("DEBUG: decompressIfNeeded available:", typeof window.decompressIfNeeded === "function");
if (typeof window.decodeAnyPayload === "function") { if (typeof window.decodeAnyPayload === "function") {
console.log("DEBUG: Using decodeAnyPayload...");
const any = window.decodeAnyPayload(offerInput.trim()); const any = window.decodeAnyPayload(offerInput.trim());
console.log("DEBUG: decodeAnyPayload result type:", typeof any);
console.log("DEBUG: decodeAnyPayload result:", any);
offer = typeof any === "string" ? JSON.parse(any) : any; offer = typeof any === "string" ? JSON.parse(any) : any;
} else { } else {
console.log("DEBUG: Using decompressIfNeeded...");
const rawText = typeof window.decompressIfNeeded === "function" ? window.decompressIfNeeded(offerInput.trim()) : offerInput.trim(); const rawText = typeof window.decompressIfNeeded === "function" ? window.decompressIfNeeded(offerInput.trim()) : offerInput.trim();
console.log("DEBUG: decompressIfNeeded result:", rawText.substring(0, 100) + "...");
offer = JSON.parse(rawText); offer = JSON.parse(rawText);
} }
console.log("DEBUG: Final offer:", offer);
} catch (parseError) { } catch (parseError) {
console.error("DEBUG: Parse error:", parseError);
throw new Error(`Invalid invitation format: ${parseError.message}`); throw new Error(`Invalid invitation format: ${parseError.message}`);
} }
if (!offer || typeof offer !== "object") { if (!offer || typeof offer !== "object") {
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -12
View File
@@ -2589,7 +2589,7 @@
} }
// Single QR code - try to decode directly // Single QR code - try to decode directly
console.log('Processing single QR code:', scannedData.substring(0, 50) + '...'); // Removed verbose debug log
let parsedData; let parsedData;
if (typeof window.decodeAnyPayload === 'function') { if (typeof window.decodeAnyPayload === 'function') {
const any = window.decodeAnyPayload(scannedData); const any = window.decodeAnyPayload(scannedData);
@@ -2939,26 +2939,15 @@
let offer; let offer;
try { try {
console.log('DEBUG: Processing offer input:', offerInput.trim().substring(0, 100) + '...');
console.log('DEBUG: decodeAnyPayload available:', typeof window.decodeAnyPayload === 'function');
console.log('DEBUG: decompressIfNeeded available:', typeof window.decompressIfNeeded === 'function');
// Prefer binary decode first, then gzip JSON // Prefer binary decode first, then gzip JSON
if (typeof window.decodeAnyPayload === 'function') { if (typeof window.decodeAnyPayload === 'function') {
console.log('DEBUG: Using decodeAnyPayload...');
const any = window.decodeAnyPayload(offerInput.trim()); const any = window.decodeAnyPayload(offerInput.trim());
console.log('DEBUG: decodeAnyPayload result type:', typeof any);
console.log('DEBUG: decodeAnyPayload result:', any);
offer = (typeof any === 'string') ? JSON.parse(any) : any; offer = (typeof any === 'string') ? JSON.parse(any) : any;
} else { } else {
console.log('DEBUG: Using decompressIfNeeded...');
const rawText = (typeof window.decompressIfNeeded === 'function') ? window.decompressIfNeeded(offerInput.trim()) : offerInput.trim(); const rawText = (typeof window.decompressIfNeeded === 'function') ? window.decompressIfNeeded(offerInput.trim()) : offerInput.trim();
console.log('DEBUG: decompressIfNeeded result:', rawText.substring(0, 100) + '...');
offer = JSON.parse(rawText); offer = JSON.parse(rawText);
} }
console.log('DEBUG: Final offer:', offer);
} catch (parseError) { } catch (parseError) {
console.error('DEBUG: Parse error:', parseError);
throw new Error(`Invalid invitation format: ${parseError.message}`); throw new Error(`Invalid invitation format: ${parseError.message}`);
} }
+11 -29
View File
@@ -63,11 +63,11 @@ const EnhancedMinimalHeader = ({
} else if (window.DEBUG_MODE) { } else if (window.DEBUG_MODE) {
} }
} else { } else {
console.warn(' Security calculation returned invalid data');
} }
} catch (error) { } catch (error) {
console.error(' Error in real security calculation:', error);
} finally { } finally {
isUpdating = false; isUpdating = false;
} }
@@ -125,11 +125,11 @@ const EnhancedMinimalHeader = ({
if (securityData && securityData.isRealData !== false) { if (securityData && securityData.isRealData !== false) {
setRealSecurityLevel(securityData); setRealSecurityLevel(securityData);
setLastSecurityUpdate(Date.now()); setLastSecurityUpdate(Date.now());
console.log('✅ Header security level force-updated');
} }
}) })
.catch(error => { .catch(error => {
console.error('❌ Force update failed:', error);
}); });
} else { } else {
setLastSecurityUpdate(0); setLastSecurityUpdate(0);
@@ -170,9 +170,7 @@ const EnhancedMinimalHeader = ({
// Connection cleanup handler (use existing event from module) // Connection cleanup handler (use existing event from module)
const handleConnectionCleaned = () => { const handleConnectionCleaned = () => {
if (window.DEBUG_MODE) {
console.log('🧹 Connection cleaned - clearing security data in header');
}
setRealSecurityLevel(null); setRealSecurityLevel(null);
setLastSecurityUpdate(0); setLastSecurityUpdate(0);
@@ -183,9 +181,7 @@ const EnhancedMinimalHeader = ({
}; };
const handlePeerDisconnect = () => { const handlePeerDisconnect = () => {
if (window.DEBUG_MODE) {
console.log('👋 Peer disconnect detected - clearing security data in header');
}
setRealSecurityLevel(null); setRealSecurityLevel(null);
setLastSecurityUpdate(0); setLastSecurityUpdate(0);
@@ -236,15 +232,12 @@ const EnhancedMinimalHeader = ({
if (webrtcManager && window.EnhancedSecureCryptoUtils) { if (webrtcManager && window.EnhancedSecureCryptoUtils) {
try { try {
realTestResults = await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(webrtcManager); realTestResults = await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(webrtcManager);
console.log('✅ Real security tests completed:', realTestResults);
} catch (error) { } catch (error) {
console.error('❌ Real security tests failed:', error);
} }
} else { } else {
console.log('⚠️ Cannot run security tests:', {
webrtcManager: !!webrtcManager,
cryptoUtils: !!window.EnhancedSecureCryptoUtils
});
} }
// If no real test results and no existing security level, show progress message // If no real test results and no existing security level, show progress message
@@ -269,7 +262,7 @@ const EnhancedMinimalHeader = ({
passedChecks: 0, passedChecks: 0,
totalChecks: 0 totalChecks: 0
}; };
console.log('Using fallback security data:', securityData);
} }
// Detailed information about the REAL security check // Detailed information about the REAL security check
@@ -501,18 +494,7 @@ const EnhancedMinimalHeader = ({
// ============================================ // ============================================
React.useEffect(() => { React.useEffect(() => {
window.debugHeaderSecurity = () => { window.debugHeaderSecurity = undefined;
console.log('🔍 Header Security Debug:', {
realSecurityLevel,
lastSecurityUpdate,
isConnected,
webrtcManagerProp: !!webrtcManager,
windowWebrtcManager: !!window.webrtcManager,
cryptoUtils: !!window.EnhancedSecureCryptoUtils,
displaySecurityLevel: displaySecurityLevel,
securityDetails: securityDetails
});
};
return () => { return () => {
delete window.debugHeaderSecurity; delete window.debugHeaderSecurity;
+1 -6
View File
@@ -2308,12 +2308,7 @@ class EnhancedSecureCryptoUtils {
payload.mac = Array.from(new Uint8Array(mac)); payload.mac = Array.from(new Uint8Array(mac));
EnhancedSecureCryptoUtils.secureLog.log('info', 'Message encrypted with metadata protection', { // Logging removed to avoid noisy console output
messageId,
sequenceNumber,
hasMetadataProtection: true,
hasPadding: true
});
return payload; return payload;
} catch (error) { } catch (error) {
+34 -58
View File
@@ -12094,11 +12094,10 @@ class SecureKeyStorage {
/** /**
* Get master key (with automatic unlock if needed) * Get master key (with automatic unlock if needed)
*/ */
async _getMasterKey() { async _ensureMasterKeyUnlocked() {
if (!this._masterKeyManager.isUnlocked()) { if (!this._masterKeyManager.isUnlocked()) {
await this._masterKeyManager.unlock(); await this._masterKeyManager.unlock();
} }
return this._masterKeyManager.getMasterKey();
} }
async storeKey(keyId, cryptoKey, metadata = {}) { async storeKey(keyId, cryptoKey, metadata = {}) {
@@ -12191,20 +12190,12 @@ class SecureKeyStorage {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const data = encoder.encode(dataToEncrypt); const data = encoder.encode(dataToEncrypt);
const iv = crypto.getRandomValues(new Uint8Array(12)); await this._ensureMasterKeyUnlocked();
const { encryptedData, iv } = await this._masterKeyManager.encryptBytes(data);
const masterKey = await this._getMasterKey();
const encryptedData = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
masterKey,
data
);
// Return IV + encrypted data // Return IV + encrypted data
const result = new Uint8Array(iv.length + encryptedData.byteLength); const result = new Uint8Array(iv.length + encryptedData.byteLength);
result.set(iv, 0); result.set(iv, 0);
result.set(new Uint8Array(encryptedData), iv.length); result.set(encryptedData, iv.length);
return result; return result;
} }
@@ -12212,12 +12203,8 @@ class SecureKeyStorage {
const iv = encryptedData.slice(0, 12); const iv = encryptedData.slice(0, 12);
const data = encryptedData.slice(12); const data = encryptedData.slice(12);
const masterKey = await this._getMasterKey(); await this._ensureMasterKeyUnlocked();
const decryptedData = await crypto.subtle.decrypt( const decryptedData = await this._masterKeyManager.decryptBytes(data, iv);
{ name: 'AES-GCM', iv },
masterKey,
data
);
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const jsonString = decoder.decode(decryptedData); const jsonString = decoder.decode(decryptedData);
@@ -12906,11 +12893,8 @@ class SecurePersistentKeyStorage {
// Export key to JWK // Export key to JWK
const jwkData = await crypto.subtle.exportKey('jwk', cryptoKey); const jwkData = await crypto.subtle.exportKey('jwk', cryptoKey);
// Get master key for encryption
const masterKey = this._masterKeyManager.getMasterKey();
// Encrypt JWK data // Encrypt JWK data
const { encryptedData, iv } = await this._encryptKeyData(jwkData, masterKey); const { encryptedData, iv } = await this._encryptKeyData(jwkData);
// Store encrypted data in IndexedDB // Store encrypted data in IndexedDB
await this._indexedDB.storeEncryptedKey( await this._indexedDB.storeEncryptedKey(
@@ -12952,11 +12936,8 @@ class SecurePersistentKeyStorage {
return null; return null;
} }
// Get master key for decryption
const masterKey = this._masterKeyManager.getMasterKey();
// Decrypt JWK data // Decrypt JWK data
const jwkData = await this._decryptKeyData(keyRecord.encryptedData, keyRecord.iv, masterKey); const jwkData = await this._decryptKeyData(keyRecord.encryptedData, keyRecord.iv);
// Import as non-extractable key // Import as non-extractable key
const restoredKey = await this._importAsNonExtractable(jwkData, keyRecord.algorithm, keyRecord.usages); const restoredKey = await this._importAsNonExtractable(jwkData, keyRecord.algorithm, keyRecord.usages);
@@ -13029,37 +13010,21 @@ class SecurePersistentKeyStorage {
/** /**
* Encrypt key data using master key * Encrypt key data using master key
*/ */
async _encryptKeyData(jwkData, masterKey) { async _encryptKeyData(jwkData) {
// Convert JWK to JSON string and then to bytes // Convert JWK to JSON string and then to bytes
const jsonString = JSON.stringify(jwkData); const jsonString = JSON.stringify(jwkData);
const data = new TextEncoder().encode(jsonString); const data = new TextEncoder().encode(jsonString);
// Generate random IV await this._ensureMasterKeyUnlocked();
const iv = crypto.getRandomValues(new Uint8Array(12)); return await this._masterKeyManager.encryptBytes(data);
// Encrypt with AES-GCM
const encryptedData = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
masterKey,
data
);
return {
encryptedData: new Uint8Array(encryptedData),
iv: iv
};
} }
/** /**
* Decrypt key data using master key * Decrypt key data using master key
*/ */
async _decryptKeyData(encryptedData, iv, masterKey) { async _decryptKeyData(encryptedData, iv) {
// Decrypt with AES-GCM await this._ensureMasterKeyUnlocked();
const decryptedData = await crypto.subtle.decrypt( const decryptedData = await this._masterKeyManager.decryptBytes(encryptedData, iv);
{ name: 'AES-GCM', iv },
masterKey,
encryptedData
);
// Convert back to JWK // Convert back to JWK
const jsonString = new TextDecoder().decode(decryptedData); const jsonString = new TextDecoder().decode(decryptedData);
@@ -13114,7 +13079,7 @@ class SecurePersistentKeyStorage {
class SecureMasterKeyManager { class SecureMasterKeyManager {
constructor(indexedDBWrapper = null) { constructor(indexedDBWrapper = null) {
// Session state // Session state
this._masterKey = null; this._keyHandle = null; // non-extractable CryptoKey
this._isUnlocked = false; this._isUnlocked = false;
this._sessionTimeout = null; this._sessionTimeout = null;
this._lastActivity = null; this._lastActivity = null;
@@ -13372,8 +13337,8 @@ class SecureMasterKeyManager {
// Get or create persistent salt // Get or create persistent salt
const salt = await this._getOrCreateSalt(); const salt = await this._getOrCreateSalt();
// Derive master key // Derive non-extractable key handle
this._masterKey = await this._deriveKeyFromPassword(password, salt); this._keyHandle = await this._deriveKeyFromPassword(password, salt);
// Mark as unlocked // Mark as unlocked
this._isUnlocked = true; this._isUnlocked = true;
@@ -13408,13 +13373,24 @@ class SecureMasterKeyManager {
/** /**
* Get master key (only if unlocked) * Get master key (only if unlocked)
*/ */
getMasterKey() { // Prevent direct key access; provide operations only
if (!this._isUnlocked || !this._masterKey) { async encryptBytes(plainBytes) {
if (!this._isUnlocked || !this._keyHandle) {
throw new Error('Master key is locked'); throw new Error('Master key is locked');
} }
this._updateActivity(); this._updateActivity();
return this._masterKey; const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, this._keyHandle, plainBytes);
return { encryptedData: new Uint8Array(encrypted), iv };
}
async decryptBytes(encryptedBytes, iv) {
if (!this._isUnlocked || !this._keyHandle) {
throw new Error('Master key is locked');
}
this._updateActivity();
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, this._keyHandle, encryptedBytes);
return new Uint8Array(decrypted);
} }
/** /**
@@ -13440,10 +13416,10 @@ class SecureMasterKeyManager {
* Securely wipe master key from memory * Securely wipe master key from memory
*/ */
_secureWipeMasterKey() { _secureWipeMasterKey() {
if (this._masterKey) { if (this._keyHandle) {
// CryptoKey objects are automatically garbage collected // CryptoKey objects are automatically garbage collected
// but we clear the reference immediately // but we clear the reference immediately
this._masterKey = null; this._keyHandle = null;
} }
this._clearTimers(); this._clearTimers();
} }