1
0
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:
Matt Gibson
2021-06-09 15:53:54 -05:00
committed by GitHub
parent d7682cde3b
commit 5ba1416679
15 changed files with 188 additions and 73 deletions

View File

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

View File

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

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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',

View File

@@ -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);