mirror of
https://github.com/bitwarden/jslib
synced 2025-12-06 00:03:29 +00:00
Authenticate with secure storage service (#402)
* Split secure key into use case Allows us to push authentication for key access as late as possible. * Do not reload if biometric locked * Linter fixes * Fix key upgrade scenario * Fix boolean value message parsing * Handle systems which don't support biometrics * Do not fail key retrieval on secret upgrade * Ensure old key is removed regardless of upgrade success * Log errors
This commit is contained in:
@@ -16,7 +16,7 @@ export default class BiometricDarwinMain implements BiometricMain {
|
||||
this.storageService.save(ElectronConstants.noAutoPromptBiometricsText, 'noAutoPromptTouchId');
|
||||
|
||||
ipcMain.on('biometric', async (event: any, message: any) => {
|
||||
event.returnValue = await this.requestCreate();
|
||||
event.returnValue = await this.authenticateBiometric();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class BiometricDarwinMain implements BiometricMain {
|
||||
return Promise.resolve(systemPreferences.canPromptTouchID());
|
||||
}
|
||||
|
||||
async requestCreate(): Promise<boolean> {
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
try {
|
||||
await systemPreferences.promptTouchID(this.i18nservice.t('touchIdConsentMessage'));
|
||||
return true;
|
||||
|
||||
@@ -30,7 +30,7 @@ export default class BiometricWindowsMain implements BiometricMain {
|
||||
this.storageService.save(ElectronConstants.noAutoPromptBiometricsText, 'noAutoPromptWindowsHello');
|
||||
|
||||
ipcMain.on('biometric', async (event: any, message: any) => {
|
||||
event.returnValue = await this.requestCreate();
|
||||
event.returnValue = await this.authenticateBiometric();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export default class BiometricWindowsMain implements BiometricMain {
|
||||
return this.getAllowedAvailabilities().includes(availability);
|
||||
}
|
||||
|
||||
async requestCreate(): Promise<boolean> {
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
const module = this.getWindowsSecurityCredentialsUiModule();
|
||||
if (module == null) {
|
||||
return false;
|
||||
|
||||
@@ -6,27 +6,51 @@ import {
|
||||
setPassword,
|
||||
} from 'keytar';
|
||||
|
||||
import { BiometricMain } from 'jslib-common/abstractions/biometric.main';
|
||||
|
||||
const AuthRequiredSuffix = '_biometric';
|
||||
const AuthenticatedActions = ['getPassword'];
|
||||
|
||||
export class KeytarStorageListener {
|
||||
constructor(private serviceName: string) { }
|
||||
constructor(private serviceName: string, private biometricService: BiometricMain) { }
|
||||
|
||||
init() {
|
||||
ipcMain.on('keytar', async (event: any, message: any) => {
|
||||
try {
|
||||
let val: string = null;
|
||||
if (message.action && message.key) {
|
||||
if (message.action === 'getPassword') {
|
||||
val = await getPassword(this.serviceName, message.key);
|
||||
} else if (message.action === 'setPassword' && message.value) {
|
||||
await setPassword(this.serviceName, message.key, message.value);
|
||||
} else if (message.action === 'deletePassword') {
|
||||
await deletePassword(this.serviceName, message.key);
|
||||
}
|
||||
let serviceName = this.serviceName;
|
||||
message.keySuffix = '_' + (message.keySuffix ?? '');
|
||||
if (message.keySuffix !== '_') {
|
||||
serviceName += message.keySuffix;
|
||||
}
|
||||
|
||||
const authenticationRequired = AuthenticatedActions.includes(message.action) &&
|
||||
AuthRequiredSuffix === message.keySuffix;
|
||||
const authenticated = !authenticationRequired || await this.authenticateBiometric();
|
||||
|
||||
let val: string | boolean = null;
|
||||
if (authenticated && message.action && message.key) {
|
||||
if (message.action === 'getPassword') {
|
||||
val = await getPassword(serviceName, message.key);
|
||||
} else if (message.action === 'hasPassword') {
|
||||
const result = await getPassword(serviceName, message.key);
|
||||
val = result != null;
|
||||
} else if (message.action === 'setPassword' && message.value) {
|
||||
await setPassword(serviceName, message.key, message.value);
|
||||
} else if (message.action === 'deletePassword') {
|
||||
await deletePassword(serviceName, message.key);
|
||||
}
|
||||
}
|
||||
event.returnValue = val;
|
||||
} catch {
|
||||
event.returnValue = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async authenticateBiometric(): Promise<boolean> {
|
||||
if (this.biometricService) {
|
||||
return await this.biometricService.authenticateBiometric();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,41 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
||||
import { StorageService, StorageServiceOptions } from 'jslib-common/abstractions/storage.service';
|
||||
|
||||
export class ElectronRendererSecureStorageService implements StorageService {
|
||||
async get<T>(key: string): Promise<T> {
|
||||
async get<T>(key: string, options?: StorageServiceOptions): Promise<T> {
|
||||
const val = ipcRenderer.sendSync('keytar', {
|
||||
action: 'getPassword',
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? '',
|
||||
});
|
||||
return Promise.resolve(val != null ? JSON.parse(val) as T : null);
|
||||
}
|
||||
|
||||
async save(key: string, obj: any): Promise<any> {
|
||||
async has(key: string, options?: StorageServiceOptions): Promise<boolean> {
|
||||
const val = ipcRenderer.sendSync('keytar', {
|
||||
action: 'hasPassword',
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? '',
|
||||
});
|
||||
return Promise.resolve(!!val);
|
||||
}
|
||||
|
||||
async save(key: string, obj: any, options?: StorageServiceOptions): Promise<any> {
|
||||
ipcRenderer.sendSync('keytar', {
|
||||
action: 'setPassword',
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? '',
|
||||
value: JSON.stringify(obj),
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<any> {
|
||||
async remove(key: string, options?: StorageServiceOptions): Promise<any> {
|
||||
ipcRenderer.sendSync('keytar', {
|
||||
action: 'deletePassword',
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? '',
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@ export class ElectronRendererStorageService implements StorageService {
|
||||
});
|
||||
}
|
||||
|
||||
has(key: string): Promise<boolean> {
|
||||
return ipcRenderer.invoke('storageService', {
|
||||
action: 'has',
|
||||
key: key,
|
||||
});
|
||||
}
|
||||
|
||||
save(key: string, obj: any): Promise<any> {
|
||||
return ipcRenderer.invoke('storageService', {
|
||||
action: 'save',
|
||||
|
||||
@@ -25,6 +25,8 @@ export class ElectronStorageService implements StorageService {
|
||||
switch (options.action) {
|
||||
case 'get':
|
||||
return this.get(options.key);
|
||||
case 'has':
|
||||
return this.has(options.key);
|
||||
case 'save':
|
||||
return this.save(options.key, options.obj);
|
||||
case 'remove':
|
||||
@@ -38,6 +40,11 @@ export class ElectronStorageService implements StorageService {
|
||||
return Promise.resolve(val != null ? val : null);
|
||||
}
|
||||
|
||||
has(key: string): Promise<boolean> {
|
||||
const val = this.store.get(key);
|
||||
return Promise.resolve(val != null);
|
||||
}
|
||||
|
||||
save(key: string, obj: any): Promise<any> {
|
||||
if (obj instanceof Set) {
|
||||
obj = Array.from(obj);
|
||||
|
||||
Reference in New Issue
Block a user