[PS-1134] Folder fixes, including revamped auth logic (#3118)

This commit is contained in:
Oscar Hinton 2022-07-18 14:39:12 +02:00 committed by GitHub
parent cd5aef1757
commit fbff2e5f00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 65 additions and 49 deletions

View File

@ -44,7 +44,6 @@ import { ApiService } from "@bitwarden/common/services/api.service";
import { AppIdService } from "@bitwarden/common/services/appId.service"; import { AppIdService } from "@bitwarden/common/services/appId.service";
import { AuditService } from "@bitwarden/common/services/audit.service"; import { AuditService } from "@bitwarden/common/services/audit.service";
import { AuthService } from "@bitwarden/common/services/auth.service"; import { AuthService } from "@bitwarden/common/services/auth.service";
import { BroadcasterService } from "@bitwarden/common/services/broadcaster.service";
import { CipherService } from "@bitwarden/common/services/cipher.service"; import { CipherService } from "@bitwarden/common/services/cipher.service";
import { CollectionService } from "@bitwarden/common/services/collection.service"; import { CollectionService } from "@bitwarden/common/services/collection.service";
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service"; import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
@ -150,7 +149,6 @@ export default class MainBackground {
vaultFilterService: VaultFilterService; vaultFilterService: VaultFilterService;
usernameGenerationService: UsernameGenerationServiceAbstraction; usernameGenerationService: UsernameGenerationServiceAbstraction;
encryptService: EncryptService; encryptService: EncryptService;
broadcasterService: BroadcasterService;
folderApiService: FolderApiServiceAbstraction; folderApiService: FolderApiServiceAbstraction;
// Passed to the popup for Safari to workaround issues with theming, downloading, etc. // Passed to the popup for Safari to workaround issues with theming, downloading, etc.
@ -272,13 +270,11 @@ export default class MainBackground {
this.logService, this.logService,
this.stateService this.stateService
); );
this.broadcasterService = new BroadcasterService();
this.folderService = new FolderService( this.folderService = new FolderService(
this.cryptoService, this.cryptoService,
this.i18nService, this.i18nService,
this.cipherService, this.cipherService,
this.stateService, this.stateService
this.broadcasterService
); );
this.folderApiService = new FolderApiService(this.folderService, this.apiService); this.folderApiService = new FolderApiService(this.folderService, this.apiService);
this.collectionService = new CollectionService( this.collectionService = new CollectionService(

View File

@ -13,7 +13,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AppIdService } from "@bitwarden/common/abstractions/appId.service"; import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/abstractions/auth.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/abstractions/auth.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/abstractions/broadcaster.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
@ -114,10 +113,6 @@ function getBgService<T>(service: keyof MainBackground) {
: new BrowserMessagingService(); : new BrowserMessagingService();
}, },
}, },
{
provide: BroadcasterServiceAbstraction,
useFactory: getBgService<BroadcasterServiceAbstraction>("broadcasterService"),
},
{ {
provide: TwoFactorService, provide: TwoFactorService,
useFactory: getBgService<TwoFactorService>("twoFactorService"), useFactory: getBgService<TwoFactorService>("twoFactorService"),

View File

@ -206,8 +206,7 @@ export class Main {
this.cryptoService, this.cryptoService,
this.i18nService, this.i18nService,
this.cipherService, this.cipherService,
this.stateService, this.stateService
this.broadcasterService
); );
this.folderApiService = new FolderApiService(this.folderService, this.apiService); this.folderApiService = new FolderApiService(this.folderService, this.apiService);

View File

@ -1,5 +1,3 @@
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
@ -360,7 +358,7 @@ export class GetCommand extends DownloadCommand {
decFolder = await folder.decrypt(); decFolder = await folder.decrypt();
} }
} else if (id.trim() !== "") { } else if (id.trim() !== "") {
let folders = await firstValueFrom(this.folderService.folderViews$); let folders = await this.folderService.getAllDecryptedFromState();
folders = CliUtils.searchFolders(folders, id); folders = CliUtils.searchFolders(folders, id);
if (folders.length > 1) { if (folders.length > 1) {
return Response.multipleResults(folders.map((f) => f.id)); return Response.multipleResults(folders.map((f) => f.id));

View File

@ -1,5 +1,3 @@
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
@ -128,7 +126,7 @@ export class ListCommand {
} }
private async listFolders(options: Options) { private async listFolders(options: Options) {
let folders = await firstValueFrom(this.folderService.folderViews$); let folders = await this.folderService.getAllDecryptedFromState();
if (options.search != null && options.search.trim() !== "") { if (options.search != null && options.search.trim() !== "") {
folders = CliUtils.searchFolders(folders, options.search); folders = CliUtils.searchFolders(folders, options.search);

View File

@ -1,5 +1,5 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { from, mergeMap, Observable } from "rxjs"; import { firstValueFrom, from, mergeMap, Observable } from "rxjs";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
@ -90,7 +90,7 @@ export class VaultFilterService {
return await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership); return await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership);
} }
protected async getAllFoldersNested(folders?: FolderView[]): Promise<TreeNode<FolderView>[]> { protected async getAllFoldersNested(folders: FolderView[]): Promise<TreeNode<FolderView>[]> {
const nodes: TreeNode<FolderView>[] = []; const nodes: TreeNode<FolderView>[] = [];
folders.forEach((f) => { folders.forEach((f) => {
const folderCopy = new FolderView(); const folderCopy = new FolderView();
@ -103,7 +103,9 @@ export class VaultFilterService {
} }
async getFolderNested(id: string): Promise<TreeNode<FolderView>> { async getFolderNested(id: string): Promise<TreeNode<FolderView>> {
const folders = await this.getAllFoldersNested(); const folders = await this.getAllFoldersNested(
await firstValueFrom(this.folderService.folderViews$)
);
return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode<FolderView>; return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode<FolderView>;
} }
} }

View File

@ -223,7 +223,6 @@ export const LOG_MAC_FAILURES = new InjectionToken<string>("LOG_MAC_FAILURES");
I18nServiceAbstraction, I18nServiceAbstraction,
CipherServiceAbstraction, CipherServiceAbstraction,
StateServiceAbstraction, StateServiceAbstraction,
BroadcasterServiceAbstraction,
], ],
}, },
{ {

View File

@ -12,6 +12,10 @@ export abstract class FolderService {
clearCache: () => Promise<void>; clearCache: () => Promise<void>;
encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise<Folder>; encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise<Folder>;
get: (id: string) => Promise<Folder>; get: (id: string) => Promise<Folder>;
/**
* @deprecated Only use in CLI!
*/
getAllDecryptedFromState: () => Promise<FolderView[]>;
} }
export abstract class InternalFolderService extends FolderService { export abstract class InternalFolderService extends FolderService {

View File

@ -1,4 +1,4 @@
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject, Observable } from "rxjs";
import { KdfType } from "../enums/kdfType"; import { KdfType } from "../enums/kdfType";
import { ThemeType } from "../enums/themeType"; import { ThemeType } from "../enums/themeType";
@ -28,6 +28,8 @@ export abstract class StateService<T extends Account = Account> {
accounts: BehaviorSubject<{ [userId: string]: T }>; accounts: BehaviorSubject<{ [userId: string]: T }>;
activeAccount: BehaviorSubject<string>; activeAccount: BehaviorSubject<string>;
activeAccountUnlocked: Observable<boolean>;
addAccount: (account: T) => Promise<void>; addAccount: (account: T) => Promise<void>;
setActiveUser: (userId: string) => Promise<void>; setActiveUser: (userId: string) => Promise<void>;
clean: (options?: StorageOptions) => Promise<void>; clean: (options?: StorageOptions) => Promise<void>;

View File

@ -1,9 +1,8 @@
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { BroadcasterService } from "../../abstractions/broadcaster.service";
import { CipherService } from "../../abstractions/cipher.service"; import { CipherService } from "../../abstractions/cipher.service";
import { CryptoService } from "../../abstractions/crypto.service"; import { CryptoService } from "../../abstractions/crypto.service";
import { FolderService as FolderServiceAbstraction } from "../../abstractions/folder/folder.service.abstraction"; import { InternalFolderService as InternalFolderServiceAbstraction } from "../../abstractions/folder/folder.service.abstraction";
import { I18nService } from "../../abstractions/i18n.service"; import { I18nService } from "../../abstractions/i18n.service";
import { StateService } from "../../abstractions/state.service"; import { StateService } from "../../abstractions/state.service";
import { Utils } from "../../misc/utils"; import { Utils } from "../../misc/utils";
@ -13,9 +12,7 @@ import { Folder } from "../../models/domain/folder";
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey"; import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
import { FolderView } from "../../models/view/folderView"; import { FolderView } from "../../models/view/folderView";
const BroadcasterSubscriptionId = "FolderService"; export class FolderService implements InternalFolderServiceAbstraction {
export class FolderService implements FolderServiceAbstraction {
private _folders: BehaviorSubject<Folder[]> = new BehaviorSubject([]); private _folders: BehaviorSubject<Folder[]> = new BehaviorSubject([]);
private _folderViews: BehaviorSubject<FolderView[]> = new BehaviorSubject([]); private _folderViews: BehaviorSubject<FolderView[]> = new BehaviorSubject([]);
@ -26,15 +23,14 @@ export class FolderService implements FolderServiceAbstraction {
private cryptoService: CryptoService, private cryptoService: CryptoService,
private i18nService: I18nService, private i18nService: I18nService,
private cipherService: CipherService, private cipherService: CipherService,
private stateService: StateService, private stateService: StateService
private broadcasterService: BroadcasterService
) { ) {
this.stateService.activeAccount.subscribe(async (activeAccount) => { this.stateService.activeAccountUnlocked.subscribe(async (unlocked) => {
if ((Utils.global as any).bitwardenContainerService == null) { if ((Utils.global as any).bitwardenContainerService == null) {
return; return;
} }
if (activeAccount == null) { if (!unlocked) {
this._folders.next([]); this._folders.next([]);
this._folderViews.next([]); this._folderViews.next([]);
return; return;
@ -44,20 +40,6 @@ export class FolderService implements FolderServiceAbstraction {
await this.updateObservables(data); await this.updateObservables(data);
}); });
// TODO: Broadcasterservice should be removed or replaced with observables
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
switch (message.command) {
case "unlocked": {
const data = await this.stateService.getEncryptedFolders();
await this.updateObservables(data);
break;
}
default:
break;
}
});
} }
async clearCache(): Promise<void> { async clearCache(): Promise<void> {
@ -78,6 +60,16 @@ export class FolderService implements FolderServiceAbstraction {
return folders.find((folder) => folder.id === id); return folders.find((folder) => folder.id === id);
} }
/**
* @deprecated Only use in CLI!
*/
async getAllDecryptedFromState(): Promise<FolderView[]> {
const data = await this.stateService.getEncryptedFolders();
const folders = Object.values(data || {}).map((f) => new Folder(f));
return this.decryptFolders(folders);
}
async upsert(folder: FolderData | FolderData[]): Promise<void> { async upsert(folder: FolderData | FolderData[]): Promise<void> {
let folders = await this.stateService.getEncryptedFolders(); let folders = await this.stateService.getEncryptedFolders();
if (folders == null) { if (folders == null) {
@ -149,6 +141,14 @@ export class FolderService implements FolderServiceAbstraction {
private async updateObservables(foldersMap: { [id: string]: FolderData }) { private async updateObservables(foldersMap: { [id: string]: FolderData }) {
const folders = Object.values(foldersMap || {}).map((f) => new Folder(f)); const folders = Object.values(foldersMap || {}).map((f) => new Folder(f));
this._folders.next(folders);
if (await this.cryptoService.hasKey()) {
this._folderViews.next(await this.decryptFolders(folders));
}
}
private async decryptFolders(folders: Folder[]) {
const decryptFolderPromises = folders.map((f) => f.decrypt()); const decryptFolderPromises = folders.map((f) => f.decrypt());
const decryptedFolders = await Promise.all(decryptFolderPromises); const decryptedFolders = await Promise.all(decryptFolderPromises);
@ -158,7 +158,6 @@ export class FolderService implements FolderServiceAbstraction {
noneFolder.name = this.i18nService.t("noneFolder"); noneFolder.name = this.i18nService.t("noneFolder");
decryptedFolders.push(noneFolder); decryptedFolders.push(noneFolder);
this._folders.next(folders); return decryptedFolders;
this._folderViews.next(decryptedFolders);
} }
} }

View File

@ -56,6 +56,7 @@ export class StateService<
{ {
accounts = new BehaviorSubject<{ [userId: string]: TAccount }>({}); accounts = new BehaviorSubject<{ [userId: string]: TAccount }>({});
activeAccount = new BehaviorSubject<string>(null); activeAccount = new BehaviorSubject<string>(null);
activeAccountUnlocked = new BehaviorSubject<boolean>(false);
private hasBeenInited = false; private hasBeenInited = false;
private isRecoveredSession = false; private isRecoveredSession = false;
@ -70,7 +71,21 @@ export class StateService<
protected stateMigrationService: StateMigrationService, protected stateMigrationService: StateMigrationService,
protected stateFactory: StateFactory<TGlobalState, TAccount>, protected stateFactory: StateFactory<TGlobalState, TAccount>,
protected useAccountCache: boolean = true protected useAccountCache: boolean = true
) {} ) {
// If the account gets changed, verify the new account is unlocked
this.activeAccount.subscribe(async (userId) => {
if (userId == null && this.activeAccountUnlocked.getValue() == false) {
return;
} else if (userId == null) {
this.activeAccountUnlocked.next(false);
}
// FIXME: This should be refactored into AuthService or a similar service,
// as checking for the existance of the crypto key is a low level
// implementation detail.
this.activeAccountUnlocked.next((await this.getCryptoMasterKey()) != null);
});
}
async init(): Promise<void> { async init(): Promise<void> {
if (this.hasBeenInited) { if (this.hasBeenInited) {
@ -499,6 +514,15 @@ export class StateService<
account, account,
this.reconcileOptions(options, await this.defaultInMemoryOptions()) this.reconcileOptions(options, await this.defaultInMemoryOptions())
); );
if (options.userId == this.activeAccount.getValue()) {
const nextValue = value != null;
// Avoid emitting if we are already unlocked
if (this.activeAccountUnlocked.getValue() != nextValue) {
this.activeAccountUnlocked.next(nextValue);
}
}
} }
async getCryptoMasterKeyAuto(options?: StorageOptions): Promise<string> { async getCryptoMasterKeyAuto(options?: StorageOptions): Promise<string> {