1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 02:03:39 +00:00
Files
browser/libs/common/src/auth/services/account.service.ts
Matt Gibson 56bffb04bb Ps/pm 5533/migrate decrypted user key (#7970)
* Move user key memory state to state providers

Note: state service observable change is because these updates are no longer internal to the class, but reporter directly to account service through crypto service on update of a user key

* remove decrypted user key state

Note, we're going to move the encrypted cryptoSymmetric key (and associated master key encrypted user keys)  as part of the master key service creation. Crypto service will no longer be responsible for the encrypted forms of user key.

* Deprecate notices belong on abstraction

* Allow for single-direction status updates

This is necessary since we don't want to have to guarantee that the update to logged out occurs after the update to locked.

* Remove deprecated subject

It turns out the set for cryptoMasterKey was also unused 🎉
2024-02-22 15:07:26 -05:00

161 lines
4.8 KiB
TypeScript

import { Subject, combineLatestWith, map, distinctUntilChanged, shareReplay } from "rxjs";
import {
AccountInfo,
InternalAccountService,
accountInfoEqual,
} from "../../auth/abstractions/account.service";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import {
ACCOUNT_MEMORY,
GlobalState,
GlobalStateProvider,
KeyDefinition,
} from "../../platform/state";
import { UserId } from "../../types/guid";
import { AuthenticationStatus } from "../enums/authentication-status";
export const ACCOUNT_ACCOUNTS = KeyDefinition.record<AccountInfo, UserId>(
ACCOUNT_MEMORY,
"accounts",
{
deserializer: (accountInfo) => accountInfo,
},
);
export const ACCOUNT_ACTIVE_ACCOUNT_ID = new KeyDefinition(ACCOUNT_MEMORY, "activeAccountId", {
deserializer: (id: UserId) => id,
});
export class AccountServiceImplementation implements InternalAccountService {
private lock = new Subject<UserId>();
private logout = new Subject<UserId>();
private accountsState: GlobalState<Record<UserId, AccountInfo>>;
private activeAccountIdState: GlobalState<UserId | undefined>;
accounts$;
activeAccount$;
accountLock$ = this.lock.asObservable();
accountLogout$ = this.logout.asObservable();
constructor(
private messagingService: MessagingService,
private logService: LogService,
private globalStateProvider: GlobalStateProvider,
) {
this.accountsState = this.globalStateProvider.get(ACCOUNT_ACCOUNTS);
this.activeAccountIdState = this.globalStateProvider.get(ACCOUNT_ACTIVE_ACCOUNT_ID);
this.accounts$ = this.accountsState.state$.pipe(
map((accounts) => (accounts == null ? {} : accounts)),
);
this.activeAccount$ = this.activeAccountIdState.state$.pipe(
combineLatestWith(this.accounts$),
map(([id, accounts]) => (id ? { id, ...accounts[id] } : undefined)),
distinctUntilChanged((a, b) => a?.id === b?.id && accountInfoEqual(a, b)),
shareReplay({ bufferSize: 1, refCount: false }),
);
}
async addAccount(userId: UserId, accountData: AccountInfo): Promise<void> {
await this.accountsState.update((accounts) => {
accounts ||= {};
accounts[userId] = accountData;
return accounts;
});
}
async setAccountName(userId: UserId, name: string): Promise<void> {
await this.setAccountInfo(userId, { name });
}
async setAccountEmail(userId: UserId, email: string): Promise<void> {
await this.setAccountInfo(userId, { email });
}
async setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise<void> {
await this.setAccountInfo(userId, { status });
if (status === AuthenticationStatus.LoggedOut) {
this.logout.next(userId);
} else if (status === AuthenticationStatus.Locked) {
this.lock.next(userId);
}
}
async setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise<void> {
await this.accountsState.update(
(accounts) => {
accounts[userId].status = maxStatus;
return accounts;
},
{
shouldUpdate: (accounts) => {
if (accounts?.[userId] == null) {
throw new Error("Account does not exist");
}
return accounts[userId].status > maxStatus;
},
},
);
}
async switchAccount(userId: UserId): Promise<void> {
await this.activeAccountIdState.update(
(_, accounts) => {
if (userId == null) {
// indicates no account is active
return null;
}
if (accounts?.[userId] == null) {
throw new Error("Account does not exist");
}
return userId;
},
{
combineLatestWith: this.accounts$,
shouldUpdate: (id) => {
// update only if userId changes
return id !== userId;
},
},
);
}
// TODO: update to use our own account status settings. Requires inverting direction of state service accounts flow
async delete(): Promise<void> {
try {
this.messagingService?.send("logout");
} catch (e) {
this.logService.error(e);
throw e;
}
}
private async setAccountInfo(userId: UserId, update: Partial<AccountInfo>): Promise<void> {
function newAccountInfo(oldAccountInfo: AccountInfo): AccountInfo {
return { ...oldAccountInfo, ...update };
}
await this.accountsState.update(
(accounts) => {
accounts[userId] = newAccountInfo(accounts[userId]);
return accounts;
},
{
// Avoid unnecessary updates
// TODO: Faster comparison, maybe include a hash on the objects?
shouldUpdate: (accounts) => {
if (accounts?.[userId] == null) {
throw new Error("Account does not exist");
}
return !accountInfoEqual(accounts[userId], newAccountInfo(accounts[userId]));
},
},
);
}
}