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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 188 additions and 73 deletions

View File

@ -51,7 +51,8 @@ export class LockComponent implements OnInit {
this.pinSet = await this.vaultTimeoutService.isPinLockSet(); this.pinSet = await this.vaultTimeoutService.isPinLockSet();
this.pinLock = (this.pinSet[0] && this.vaultTimeoutService.pinProtectedKey != null) || this.pinSet[1]; this.pinLock = (this.pinSet[0] && this.vaultTimeoutService.pinProtectedKey != null) || this.pinSet[1];
this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
this.biometricLock = await this.vaultTimeoutService.isBiometricLockSet() && (await this.cryptoService.hasKey() || !this.platformUtilsService.supportsSecureStorage()); this.biometricLock = await this.vaultTimeoutService.isBiometricLockSet() &&
(await this.cryptoService.hasKeyStored('biometric') || !this.platformUtilsService.supportsSecureStorage());
this.biometricText = await this.storageService.get(ConstantsService.biometricText); this.biometricText = await this.storageService.get(ConstantsService.biometricText);
this.email = await this.userService.getEmail(); this.email = await this.userService.getEmail();
let vaultUrl = this.environmentService.getWebVaultUrl(); let vaultUrl = this.environmentService.getWebVaultUrl();
@ -157,7 +158,8 @@ export class LockComponent implements OnInit {
if (!this.biometricLock) { if (!this.biometricLock) {
return; return;
} }
const success = await this.platformUtilsService.authenticateBiometric();
const success = (await this.cryptoService.getKey('biometric')) != null;
if (success) { if (success) {
await this.doContinue(); await this.doContinue();
@ -176,6 +178,8 @@ export class LockComponent implements OnInit {
private async doContinue() { private async doContinue() {
this.vaultTimeoutService.biometricLocked = false; this.vaultTimeoutService.biometricLocked = false;
this.vaultTimeoutService.everBeenUnlocked = true;
this.vaultTimeoutService.manuallyOrTimerLocked = false;
const disableFavicon = await this.storageService.get<boolean>(ConstantsService.disableFaviconKey); const disableFavicon = await this.storageService.get<boolean>(ConstantsService.disableFaviconKey);
await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon);
this.messagingService.send('unlocked'); this.messagingService.send('unlocked');

View File

@ -2,5 +2,5 @@ export abstract class BiometricMain {
isError: boolean; isError: boolean;
init: () => Promise<void>; init: () => Promise<void>;
supportsBiometric: () => Promise<boolean>; supportsBiometric: () => Promise<boolean>;
requestCreate: () => Promise<boolean>; authenticateBiometric: () => Promise<boolean>;
} }

View File

@ -5,6 +5,7 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey';
import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse';
import { KdfType } from '../enums/kdfType'; import { KdfType } from '../enums/kdfType';
import { KeySuffixOptions } from './storage.service';
export abstract class CryptoService { export abstract class CryptoService {
setKey: (key: SymmetricCryptoKey) => Promise<any>; setKey: (key: SymmetricCryptoKey) => Promise<any>;
@ -12,7 +13,7 @@ export abstract class CryptoService {
setEncKey: (encKey: string) => Promise<{}>; setEncKey: (encKey: string) => Promise<{}>;
setEncPrivateKey: (encPrivateKey: string) => Promise<{}>; setEncPrivateKey: (encPrivateKey: string) => Promise<{}>;
setOrgKeys: (orgs: ProfileOrganizationResponse[]) => Promise<{}>; setOrgKeys: (orgs: ProfileOrganizationResponse[]) => Promise<{}>;
getKey: () => Promise<SymmetricCryptoKey>; getKey: (keySuffix?: KeySuffixOptions) => Promise<SymmetricCryptoKey>;
getKeyHash: () => Promise<string>; getKeyHash: () => Promise<string>;
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>; getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
getPublicKey: () => Promise<ArrayBuffer>; getPublicKey: () => Promise<ArrayBuffer>;
@ -21,8 +22,10 @@ export abstract class CryptoService {
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>; getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>; getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
hasKey: () => Promise<boolean>; hasKey: () => Promise<boolean>;
hasKeyInMemory: () => boolean;
hasKeyStored: (keySuffix?: KeySuffixOptions) => Promise<boolean>;
hasEncKey: () => Promise<boolean>; hasEncKey: () => Promise<boolean>;
clearKey: () => Promise<any>; clearKey: (clearSecretStorage?: boolean) => Promise<any>;
clearKeyHash: () => Promise<any>; clearKeyHash: () => Promise<any>;
clearEncKey: (memoryOnly?: boolean) => Promise<any>; clearEncKey: (memoryOnly?: boolean) => Promise<any>;
clearKeyPair: (memoryOnly?: boolean) => Promise<any>; clearKeyPair: (memoryOnly?: boolean) => Promise<any>;

View File

@ -1,5 +1,12 @@
export abstract class StorageService { export abstract class StorageService {
get: <T>(key: string) => Promise<T>; get: <T>(key: string, options?: StorageServiceOptions) => Promise<T>;
save: (key: string, obj: any) => Promise<any>; has: (key: string, options?: StorageServiceOptions) => Promise<boolean>;
remove: (key: string) => Promise<any>; save: (key: string, obj: any, options?: StorageServiceOptions) => Promise<any>;
remove: (key: string, options?: StorageServiceOptions) => Promise<any>;
} }
export interface StorageServiceOptions {
keySuffix: KeySuffixOptions;
}
export type KeySuffixOptions = 'auto' | 'biometric';

View File

@ -2,6 +2,8 @@ import { EncString } from '../models/domain/encString';
export abstract class VaultTimeoutService { export abstract class VaultTimeoutService {
biometricLocked: boolean; biometricLocked: boolean;
manuallyOrTimerLocked: boolean;
everBeenUnlocked: boolean;
pinProtectedKey: EncString; pinProtectedKey: EncString;
isLocked: () => Promise<boolean>; isLocked: () => Promise<boolean>;
checkVaultTimeout: () => Promise<void>; checkVaultTimeout: () => Promise<void>;

View File

@ -13,7 +13,10 @@ import { CryptoService as CryptoServiceAbstraction } from '../abstractions/crypt
import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service';
import { LogService } from '../abstractions/log.service'; import { LogService } from '../abstractions/log.service';
import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service';
import { StorageService } from '../abstractions/storage.service'; import {
KeySuffixOptions,
StorageService,
} from '../abstractions/storage.service';
import { ConstantsService } from './constants.service'; import { ConstantsService } from './constants.service';
@ -46,11 +49,17 @@ export class CryptoService implements CryptoServiceAbstraction {
async setKey(key: SymmetricCryptoKey): Promise<any> { async setKey(key: SymmetricCryptoKey): Promise<any> {
this.key = key; this.key = key;
if (!await this.shouldStoreKey()) { if (await this.shouldStoreKey('auto')) {
return; await this.secureStorageService.save(Keys.key, key.keyB64, { keySuffix: 'auto' });
} else {
this.clearStoredKey('auto');
} }
return this.secureStorageService.save(Keys.key, key.keyB64); if (await this.shouldStoreKey('biometric')) {
await this.secureStorageService.save(Keys.key, key.keyB64, { keySuffix: 'biometric' });
} else {
this.clearStoredKey('biometric');
}
} }
setKeyHash(keyHash: string): Promise<{}> { setKeyHash(keyHash: string): Promise<{}> {
@ -86,28 +95,23 @@ export class CryptoService implements CryptoServiceAbstraction {
return this.storageService.save(Keys.encOrgKeys, orgKeys); return this.storageService.save(Keys.encOrgKeys, orgKeys);
} }
async getKey(): Promise<SymmetricCryptoKey> { async getKey(keySuffix?: KeySuffixOptions): Promise<SymmetricCryptoKey> {
if (this.key != null) { if (this.key != null) {
return this.key; return this.key;
} }
keySuffix ||= 'auto';
const key = await this.secureStorageService.get<string>(Keys.key); const key = await this.retrieveKeyFromStorage(keySuffix);
if (key != null) { if (key != null) {
if (!await this.shouldStoreKey()) {
this.logService.warning('Throwing away stored key since settings have changed');
this.secureStorageService.remove(Keys.key);
return null;
}
const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer); const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer);
if (!await this.validateKey(symmetricKey)) { if (!await this.validateKey(symmetricKey)) {
this.logService.warning('Wrong key, throwing away stored key'); this.logService.warning('Wrong key, throwing away stored key');
this.secureStorageService.remove(Keys.key); this.secureStorageService.remove(Keys.key, { keySuffix: keySuffix });
return null; return null;
} }
this.key = symmetricKey; this.setKey(symmetricKey);
} }
return key == null ? null : this.key; return key == null ? null : this.key;
@ -247,7 +251,16 @@ export class CryptoService implements CryptoServiceAbstraction {
} }
async hasKey(): Promise<boolean> { async hasKey(): Promise<boolean> {
return (await this.getKey()) != null; return this.hasKeyInMemory() || await this.hasKeyStored('auto') || await this.hasKeyStored('biometric');
}
hasKeyInMemory(): boolean {
return this.key != null;
}
async hasKeyStored(keySuffix: KeySuffixOptions): Promise<boolean> {
await this.upgradeSecurelyStoredKey();
return await this.secureStorageService.has(Keys.key, { keySuffix: keySuffix });
} }
async hasEncKey(): Promise<boolean> { async hasEncKey(): Promise<boolean> {
@ -255,9 +268,16 @@ export class CryptoService implements CryptoServiceAbstraction {
return encKey != null; return encKey != null;
} }
clearKey(): Promise<any> { async clearKey(clearSecretStorage: boolean = true): Promise<any> {
this.key = this.legacyEtmKey = null; this.key = this.legacyEtmKey = null;
return this.secureStorageService.remove(Keys.key); if (clearSecretStorage) {
this.clearStoredKey('auto');
this.clearStoredKey('biometric');
}
}
async clearStoredKey(keySuffix: KeySuffixOptions) {
await this.secureStorageService.remove(Keys.key, { keySuffix: keySuffix });
} }
clearKeyHash(): Promise<any> { clearKeyHash(): Promise<any> {
@ -305,14 +325,6 @@ export class CryptoService implements CryptoServiceAbstraction {
async toggleKey(): Promise<any> { async toggleKey(): Promise<any> {
const key = await this.getKey(); const key = await this.getKey();
const option = await this.storageService.get(ConstantsService.vaultTimeoutKey);
const biometric = await this.storageService.get(ConstantsService.biometricUnlockKey);
if ((!biometric && this.platformUtilService.supportsSecureStorage()) && (option != null || option === 0)) {
// if we have a lock option set, clear the key
await this.clearKey();
this.key = key;
return;
}
await this.setKey(key); await this.setKey(key);
} }
@ -592,11 +604,11 @@ export class CryptoService implements CryptoServiceAbstraction {
async validateKey(key: SymmetricCryptoKey) { async validateKey(key: SymmetricCryptoKey) {
try { try {
const encPrivateKey = await this.storageService.get<string>(Keys.encPrivateKey); const encPrivateKey = await this.storageService.get<string>(Keys.encPrivateKey);
if (encPrivateKey == null) { const encKey = await this.getEncKey(key);
if (encPrivateKey == null || encKey == null) {
return false; return false;
} }
const encKey = await this.getEncKey(key);
const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), encKey); const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), encKey);
await this.cryptoFunctionService.rsaExtractPublicKey(privateKey); await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
} catch (e) { } catch (e) {
@ -608,14 +620,49 @@ export class CryptoService implements CryptoServiceAbstraction {
// Helpers // Helpers
private async shouldStoreKey() { private async shouldStoreKey(keySuffix: KeySuffixOptions) {
let shouldStoreKey = false;
if (keySuffix === 'auto') {
const vaultTimeout = await this.storageService.get<number>(ConstantsService.vaultTimeoutKey); const vaultTimeout = await this.storageService.get<number>(ConstantsService.vaultTimeoutKey);
shouldStoreKey = vaultTimeout == null;
} else if (keySuffix === 'biometric') {
const biometricUnlock = await this.storageService.get<boolean>(ConstantsService.biometricUnlockKey); const biometricUnlock = await this.storageService.get<boolean>(ConstantsService.biometricUnlockKey);
shouldStoreKey = biometricUnlock && this.platformUtilService.supportsSecureStorage();
}
return shouldStoreKey;
}
const biometricsEnabled = biometricUnlock && this.platformUtilService.supportsSecureStorage(); private async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) {
const noVaultTimeout = vaultTimeout == null; await this.upgradeSecurelyStoredKey();
return noVaultTimeout || biometricsEnabled; return await this.secureStorageService.get<string>(Keys.key, { keySuffix: keySuffix });
}
/**
* @deprecated 4 Jun 2021 This is temporary upgrade method to move from a single shared stored key to
* multiple, unique stored keys for each use, e.g. never logout vs. biometric authentication.
*/
private async upgradeSecurelyStoredKey() {
// attempt key upgrade, but if we fail just delete it. Keys will be stored property upon unlock anyway.
const key = await this.secureStorageService.get<string>(Keys.key);
if (key == null) {
return;
}
try {
if (await this.shouldStoreKey('auto')) {
await this.secureStorageService.save(Keys.key, key, { keySuffix: 'auto' });
}
if (await this.shouldStoreKey('biometric')) {
await this.secureStorageService.save(Keys.key, key, { keySuffix: 'biometric' });
}
} catch (e) {
this.logService.error(`Encountered error while upgrading obsolete Bitwarden secure storage item:`);
this.logService.error(e);
}
await this.secureStorageService.remove(Keys.key);
} }
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> { private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {

View File

@ -19,7 +19,9 @@ export class SystemService implements SystemServiceAbstraction {
} }
startProcessReload(): void { startProcessReload(): void {
if (this.vaultTimeoutService.pinProtectedKey != null || this.reloadInterval != null) { if (this.vaultTimeoutService.pinProtectedKey != null ||
this.vaultTimeoutService.biometricLocked ||
this.reloadInterval != null) {
return; return;
} }
this.cancelProcessReload(); this.cancelProcessReload();

View File

@ -17,6 +17,8 @@ import { EncString } from '../models/domain/encString';
export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
pinProtectedKey: EncString = null; pinProtectedKey: EncString = null;
biometricLocked: boolean = true; biometricLocked: boolean = true;
everBeenUnlocked: boolean = false;
manuallyOrTimerLocked: boolean = false;
private inited = false; private inited = false;
@ -46,9 +48,13 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
// Keys aren't stored for a device that is locked or logged out. // Keys aren't stored for a device that is locked or logged out.
async isLocked(): Promise<boolean> { async isLocked(): Promise<boolean> {
if (await this.cryptoService.hasKeyStored('auto') && !this.everBeenUnlocked) {
await this.cryptoService.getKey('auto');
}
const hasKey = await this.cryptoService.hasKey(); const hasKey = await this.cryptoService.hasKey();
if (hasKey) { if (hasKey) {
if (await this.isBiometricLockSet() && this.biometricLocked) { if ((await this.isBiometricLockSet() && this.biometricLocked) || this.manuallyOrTimerLocked) {
return true; return true;
} }
} }
@ -102,18 +108,8 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
} }
this.biometricLocked = true; this.biometricLocked = true;
if (allowSoftLock) { this.manuallyOrTimerLocked = true;
const biometricLocked = await this.isBiometricLockSet(); await this.cryptoService.clearKey(false);
if (biometricLocked && this.platformUtilsService.supportsSecureStorage()) {
this.messagingService.send('locked');
if (this.lockedCallback != null) {
await this.lockedCallback();
}
return;
}
}
await this.cryptoService.clearKey();
await this.cryptoService.clearOrgKeys(true); await this.cryptoService.clearOrgKeys(true);
await this.cryptoService.clearKeyPair(true); await this.cryptoService.clearKeyPair(true);
await this.cryptoService.clearEncKey(true); await this.cryptoService.clearEncKey(true);

View File

@ -16,7 +16,7 @@ export default class BiometricDarwinMain implements BiometricMain {
this.storageService.save(ElectronConstants.noAutoPromptBiometricsText, 'noAutoPromptTouchId'); this.storageService.save(ElectronConstants.noAutoPromptBiometricsText, 'noAutoPromptTouchId');
ipcMain.on('biometric', async (event: any, message: any) => { 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()); return Promise.resolve(systemPreferences.canPromptTouchID());
} }
async requestCreate(): Promise<boolean> { async authenticateBiometric(): Promise<boolean> {
try { try {
await systemPreferences.promptTouchID(this.i18nservice.t('touchIdConsentMessage')); await systemPreferences.promptTouchID(this.i18nservice.t('touchIdConsentMessage'));
return true; return true;

View File

@ -30,7 +30,7 @@ export default class BiometricWindowsMain implements BiometricMain {
this.storageService.save(ElectronConstants.noAutoPromptBiometricsText, 'noAutoPromptWindowsHello'); this.storageService.save(ElectronConstants.noAutoPromptBiometricsText, 'noAutoPromptWindowsHello');
ipcMain.on('biometric', async (event: any, message: any) => { 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); return this.getAllowedAvailabilities().includes(availability);
} }
async requestCreate(): Promise<boolean> { async authenticateBiometric(): Promise<boolean> {
const module = this.getWindowsSecurityCredentialsUiModule(); const module = this.getWindowsSecurityCredentialsUiModule();
if (module == null) { if (module == null) {
return false; return false;

View File

@ -6,27 +6,51 @@ import {
setPassword, setPassword,
} from 'keytar'; } from 'keytar';
import { BiometricMain } from 'jslib-common/abstractions/biometric.main';
const AuthRequiredSuffix = '_biometric';
const AuthenticatedActions = ['getPassword'];
export class KeytarStorageListener { export class KeytarStorageListener {
constructor(private serviceName: string) { } constructor(private serviceName: string, private biometricService: BiometricMain) { }
init() { init() {
ipcMain.on('keytar', async (event: any, message: any) => { ipcMain.on('keytar', async (event: any, message: any) => {
try { try {
let val: string = null; let serviceName = this.serviceName;
if (message.action && message.key) { message.keySuffix = '_' + (message.keySuffix ?? '');
if (message.action === 'getPassword') { if (message.keySuffix !== '_') {
val = await getPassword(this.serviceName, message.key); serviceName += message.keySuffix;
} 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);
}
} }
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; event.returnValue = val;
} catch { } catch {
event.returnValue = null; 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 { 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 { 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', { const val = ipcRenderer.sendSync('keytar', {
action: 'getPassword', action: 'getPassword',
key: key, key: key,
keySuffix: options?.keySuffix ?? '',
}); });
return Promise.resolve(val != null ? JSON.parse(val) as T : null); 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', { ipcRenderer.sendSync('keytar', {
action: 'setPassword', action: 'setPassword',
key: key, key: key,
keySuffix: options?.keySuffix ?? '',
value: JSON.stringify(obj), value: JSON.stringify(obj),
}); });
return Promise.resolve(); return Promise.resolve();
} }
async remove(key: string): Promise<any> { async remove(key: string, options?: StorageServiceOptions): Promise<any> {
ipcRenderer.sendSync('keytar', { ipcRenderer.sendSync('keytar', {
action: 'deletePassword', action: 'deletePassword',
key: key, key: key,
keySuffix: options?.keySuffix ?? '',
}); });
return Promise.resolve(); 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> { save(key: string, obj: any): Promise<any> {
return ipcRenderer.invoke('storageService', { return ipcRenderer.invoke('storageService', {
action: 'save', action: 'save',

View File

@ -25,6 +25,8 @@ export class ElectronStorageService implements StorageService {
switch (options.action) { switch (options.action) {
case 'get': case 'get':
return this.get(options.key); return this.get(options.key);
case 'has':
return this.has(options.key);
case 'save': case 'save':
return this.save(options.key, options.obj); return this.save(options.key, options.obj);
case 'remove': case 'remove':
@ -38,6 +40,11 @@ export class ElectronStorageService implements StorageService {
return Promise.resolve(val != null ? val : null); 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> { save(key: string, obj: any): Promise<any> {
if (obj instanceof Set) { if (obj instanceof Set) {
obj = Array.from(obj); obj = Array.from(obj);

View File

@ -84,6 +84,10 @@ export class LowdbStorageService implements StorageService {
}); });
} }
has(key: string): Promise<boolean> {
return this.get(key).then(v => v != null);
}
save(key: string, obj: any): Promise<any> { save(key: string, obj: any): Promise<any> {
return this.lockDbFile(() => { return this.lockDbFile(() => {
this.readForNoCache(); this.readForNoCache();