1
0
mirror of https://github.com/bitwarden/jslib synced 2025-12-24 04:04:38 +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

@@ -13,7 +13,10 @@ import { CryptoService as CryptoServiceAbstraction } from '../abstractions/crypt
import { CryptoFunctionService } from '../abstractions/cryptoFunction.service';
import { LogService } from '../abstractions/log.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';
@@ -46,11 +49,17 @@ export class CryptoService implements CryptoServiceAbstraction {
async setKey(key: SymmetricCryptoKey): Promise<any> {
this.key = key;
if (!await this.shouldStoreKey()) {
return;
if (await this.shouldStoreKey('auto')) {
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<{}> {
@@ -86,28 +95,23 @@ export class CryptoService implements CryptoServiceAbstraction {
return this.storageService.save(Keys.encOrgKeys, orgKeys);
}
async getKey(): Promise<SymmetricCryptoKey> {
async getKey(keySuffix?: KeySuffixOptions): Promise<SymmetricCryptoKey> {
if (this.key != null) {
return this.key;
}
const key = await this.secureStorageService.get<string>(Keys.key);
keySuffix ||= 'auto';
const key = await this.retrieveKeyFromStorage(keySuffix);
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);
if (!await this.validateKey(symmetricKey)) {
this.logService.warning('Wrong key, throwing away stored key');
this.secureStorageService.remove(Keys.key);
this.secureStorageService.remove(Keys.key, { keySuffix: keySuffix });
return null;
}
this.key = symmetricKey;
this.setKey(symmetricKey);
}
return key == null ? null : this.key;
@@ -247,7 +251,16 @@ export class CryptoService implements CryptoServiceAbstraction {
}
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> {
@@ -255,9 +268,16 @@ export class CryptoService implements CryptoServiceAbstraction {
return encKey != null;
}
clearKey(): Promise<any> {
async clearKey(clearSecretStorage: boolean = true): Promise<any> {
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> {
@@ -305,14 +325,6 @@ export class CryptoService implements CryptoServiceAbstraction {
async toggleKey(): Promise<any> {
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);
}
@@ -592,11 +604,11 @@ export class CryptoService implements CryptoServiceAbstraction {
async validateKey(key: SymmetricCryptoKey) {
try {
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;
}
const encKey = await this.getEncKey(key);
const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), encKey);
await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
} catch (e) {
@@ -608,14 +620,49 @@ export class CryptoService implements CryptoServiceAbstraction {
// Helpers
private async shouldStoreKey() {
const vaultTimeout = await this.storageService.get<number>(ConstantsService.vaultTimeoutKey);
const biometricUnlock = await this.storageService.get<boolean>(ConstantsService.biometricUnlockKey);
private async shouldStoreKey(keySuffix: KeySuffixOptions) {
let shouldStoreKey = false;
if (keySuffix === 'auto') {
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);
shouldStoreKey = biometricUnlock && this.platformUtilService.supportsSecureStorage();
}
return shouldStoreKey;
}
const biometricsEnabled = biometricUnlock && this.platformUtilService.supportsSecureStorage();
const noVaultTimeout = vaultTimeout == null;
private async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) {
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> {