bitwarden-estensione-browser/libs/importer/src/components/lastpass/lastpass-direct-import.serv...

207 lines
7.4 KiB
TypeScript

import { Injectable, NgZone } from "@angular/core";
import { OidcClient } from "oidc-client-ts";
import { Subject, firstValueFrom } from "rxjs";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { ClientType } from "@bitwarden/common/enums";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { DialogService } from "../../../../components/src/dialog";
import { ClientInfo, Vault } from "../../importers/lastpass/access";
import { FederatedUserContext } from "../../importers/lastpass/access/models";
import { LastPassPasswordPromptComponent } from "./dialog/lastpass-password-prompt.component";
import { LastPassDirectImportUIService } from "./lastpass-direct-import-ui.service";
@Injectable({
providedIn: "root",
})
export class LastPassDirectImportService {
private vault: Vault;
private oidcClient: OidcClient;
private _ssoImportCallback$ = new Subject<{ oidcCode: string; oidcState: string }>();
ssoImportCallback$ = this._ssoImportCallback$.asObservable();
constructor(
private tokenService: TokenService,
private cryptoFunctionService: CryptoFunctionService,
private environmentService: EnvironmentService,
private appIdService: AppIdService,
private lastPassDirectImportUIService: LastPassDirectImportUIService,
private platformUtilsService: PlatformUtilsService,
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private dialogService: DialogService,
private i18nService: I18nService,
) {
this.vault = new Vault(this.cryptoFunctionService, this.tokenService);
/** TODO: remove this in favor of dedicated service */
this.broadcasterService.subscribe("LastPassDirectImportService", (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "importCallbackLastPass":
this._ssoImportCallback$.next({ oidcCode: message.code, oidcState: message.state });
break;
default:
break;
}
});
});
}
/**
* Import a LastPass account by email
* @param email
* @param includeSharedFolders
* @returns The CSV export data of the account
*/
async handleImport(email: string, includeSharedFolders: boolean): Promise<string> {
await this.verifyLastPassAccountExists(email);
if (this.isAccountFederated) {
const oidc = await this.handleFederatedLogin(email);
const csvData = await this.handleFederatedImport(
oidc.oidcCode,
oidc.oidcState,
includeSharedFolders,
);
return csvData;
}
const password = await LastPassPasswordPromptComponent.open(this.dialogService);
const csvData = await this.handleStandardImport(email, password, includeSharedFolders);
return csvData;
}
private get isAccountFederated(): boolean {
return this.vault.userType.isFederated();
}
private async verifyLastPassAccountExists(email: string) {
await this.vault.setUserTypeContext(email);
}
private async handleFederatedLogin(email: string) {
const ssoCallbackPromise = firstValueFrom(this.ssoImportCallback$);
const request = await this.createOidcSigninRequest(email);
this.platformUtilsService.launchUri(request.url);
const cancelDialogRef = this.dialogService.openSimpleDialogRef({
title: this.i18nService.t("awaitingSSO"),
content: this.i18nService.t("awaitingSSODesc"),
type: "warning",
icon: "bwi-key",
acceptButtonText: this.i18nService.t("cancel"),
cancelButtonText: null,
});
const cancelled = firstValueFrom(cancelDialogRef.closed).then((_didCancel) => {
throw Error("SSO auth cancelled");
});
return Promise.race<{
oidcCode: string;
oidcState: string;
}>([cancelled, ssoCallbackPromise]).finally(() => {
cancelDialogRef.close();
});
}
private async createOidcSigninRequest(email: string) {
this.oidcClient = new OidcClient({
authority: this.vault.userType.openIDConnectAuthorityBase,
client_id: this.vault.userType.openIDConnectClientId,
redirect_uri: this.getOidcRedirectUrl(),
response_type: "code",
scope: this.vault.userType.oidcScope,
response_mode: "query",
loadUserInfo: true,
});
return await this.oidcClient.createSigninRequest({
state: {
email,
},
nonce: await this.passwordGenerationService.generatePassword({
length: 20,
uppercase: true,
lowercase: true,
number: true,
}),
});
}
private getOidcRedirectUrlWithParams(oidcCode: string, oidcState: string) {
const redirectUri = this.oidcClient.settings.redirect_uri;
const params = "code=" + oidcCode + "&state=" + oidcState;
if (redirectUri.indexOf("bitwarden://") === 0) {
return redirectUri + "/?" + params;
}
return redirectUri + "&" + params;
}
private getOidcRedirectUrl() {
const clientType = this.platformUtilsService.getClientType();
if (clientType === ClientType.Desktop) {
return "bitwarden://import-callback-lp";
}
const webUrl = this.environmentService.getWebVaultUrl();
return webUrl + "/sso-connector.html?lp=1";
}
private async handleStandardImport(
email: string,
password: string,
includeSharedFolders: boolean,
): Promise<string> {
const clientInfo = await this.createClientInfo(email);
await this.vault.open(email, password, clientInfo, this.lastPassDirectImportUIService, {
parseSecureNotesToAccount: false,
});
return this.vault.accountsToExportedCsvString(!includeSharedFolders);
}
private async handleFederatedImport(
oidcCode: string,
oidcState: string,
includeSharedFolders: boolean,
): Promise<string> {
const response = await this.oidcClient.processSigninResponse(
this.getOidcRedirectUrlWithParams(oidcCode, oidcState),
);
const userState = response.userState as any;
const federatedUser = new FederatedUserContext();
federatedUser.idToken = response.id_token;
federatedUser.accessToken = response.access_token;
federatedUser.idpUserInfo = response.profile;
federatedUser.username = userState.email;
const clientInfo = await this.createClientInfo(federatedUser.username);
await this.vault.openFederated(federatedUser, clientInfo, this.lastPassDirectImportUIService, {
parseSecureNotesToAccount: false,
});
return this.vault.accountsToExportedCsvString(!includeSharedFolders);
}
private async createClientInfo(email: string): Promise<ClientInfo> {
const appId = await this.appIdService.getAppId();
const id = "lastpass" + appId + email;
const idHash = await this.cryptoFunctionService.hash(id, "sha256");
return ClientInfo.createClientInfo(Utils.fromBufferToHex(idHash));
}
}