1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +00:00

[PM-4195] Lastpass lib cleanup (#6636)

* Casing fixes from the original port of the code

* Add static createClientInfo and export

* Add way to transform retrieve accounts into csv format

Create ExportAccount model
accountsToExportedCsvString can transform and export csv

* Make calls needed for UI class async/awaitable

* Add helpers for SSO on the UserTypeContext

* Add additional error handling case

* Fixes for SSO login

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
Daniel James Smith
2023-10-19 21:06:01 +02:00
committed by GitHub
parent 790d666929
commit 13df63fbac
9 changed files with 116 additions and 49 deletions

View File

@@ -1 +1,2 @@
export { ClientInfo } from "./models";
export { Vault } from "./vault"; export { Vault } from "./vault";

View File

@@ -1,7 +1,13 @@
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { Platform } from "../enums"; import { Platform } from "../enums";
export class ClientInfo { export class ClientInfo {
platform: Platform; platform: Platform;
id: string; id: string;
description: string; description: string;
static createClientInfo(): ClientInfo {
return { platform: Platform.Desktop, id: Utils.newGuid(), description: "Importer" };
}
} }

View File

@@ -0,0 +1,23 @@
import { Account } from "./account";
export class ExportedAccount {
url: string;
username: string;
password: string;
totp: string;
extra: string;
name: string;
grouping: string;
fav: number;
constructor(account: Account) {
this.url = account.url;
this.username = account.username;
this.password = account.password;
this.totp = account.totp;
this.extra = account.notes;
this.name = account.name;
this.grouping = account.path === "(none)" ? null : account.path;
this.fav = account.isFavorite ? 1 : 0;
}
}

View File

@@ -1,6 +1,7 @@
export { Account } from "./account"; export { Account } from "./account";
export { Chunk } from "./chunk"; export { Chunk } from "./chunk";
export { ClientInfo } from "./client-info"; export { ClientInfo } from "./client-info";
export { ExportedAccount } from "./exported-account";
export { FederatedUserContext } from "./federated-user-context"; export { FederatedUserContext } from "./federated-user-context";
export { OobResult } from "./oob-result"; export { OobResult } from "./oob-result";
export { OtpResult } from "./otp-result"; export { OtpResult } from "./otp-result";

View File

@@ -2,24 +2,36 @@ import { IdpProvider, LastpassLoginType } from "../enums";
export class UserTypeContext { export class UserTypeContext {
type: LastpassLoginType; type: LastpassLoginType;
IdentityProviderGUID: string; identityProviderGUID: string;
IdentityProviderURL: string; identityProviderURL: string;
OpenIDConnectAuthority: string; openIDConnectAuthority: string;
OpenIDConnectClientId: string; openIDConnectClientId: string;
CompanyId: number; companyId: number;
Provider: IdpProvider; provider: IdpProvider;
PkceEnabled: boolean; pkceEnabled: boolean;
IsPasswordlessEnabled: boolean; isPasswordlessEnabled: boolean;
isFederated(): boolean { isFederated(): boolean {
return ( return (
this.type === LastpassLoginType.Federated && this.type === LastpassLoginType.Federated &&
this.hasValue(this.IdentityProviderURL) && this.hasValue(this.identityProviderURL) &&
this.hasValue(this.OpenIDConnectAuthority) && this.hasValue(this.openIDConnectAuthority) &&
this.hasValue(this.OpenIDConnectClientId) this.hasValue(this.openIDConnectClientId)
); );
} }
get oidcScope(): string {
let scope = "openid profile email";
if (this.provider === IdpProvider.PingOne) {
scope += " lastpass";
}
return scope;
}
get openIDConnectAuthorityBase(): string {
return this.openIDConnectAuthority.replace("/.well-known/openid-configuration", "");
}
private hasValue(str: string) { private hasValue(str: string) {
return str != null && str.trim() !== ""; return str != null && str.trim() !== "";
} }

View File

@@ -229,13 +229,13 @@ export class Client {
let passcode: OtpResult = null; let passcode: OtpResult = null;
switch (method) { switch (method) {
case OtpMethod.GoogleAuth: case OtpMethod.GoogleAuth:
passcode = ui.provideGoogleAuthPasscode(); passcode = await ui.provideGoogleAuthPasscode();
break; break;
case OtpMethod.MicrosoftAuth: case OtpMethod.MicrosoftAuth:
passcode = ui.provideMicrosoftAuthPasscode(); passcode = await ui.provideMicrosoftAuthPasscode();
break; break;
case OtpMethod.Yubikey: case OtpMethod.Yubikey:
passcode = ui.provideYubikeyPasscode(); passcode = await ui.provideYubikeyPasscode();
break; break;
default: default:
throw new Error("Invalid OTP method"); throw new Error("Invalid OTP method");
@@ -273,7 +273,7 @@ export class Client {
ui: Ui, ui: Ui,
rest: RestClient rest: RestClient
): Promise<Session> { ): Promise<Session> {
const answer = this.approveOob(username, parameters, ui, rest); const answer = await this.approveOob(username, parameters, ui, rest);
if (answer == OobResult.cancel) { if (answer == OobResult.cancel) {
throw new Error("Out of band step is canceled by the user"); throw new Error("Out of band step is canceled by the user");
} }
@@ -318,7 +318,12 @@ export class Client {
return session; return session;
} }
private approveOob(username: string, parameters: Map<string, string>, ui: Ui, rest: RestClient) { private async approveOob(
username: string,
parameters: Map<string, string>,
ui: Ui,
rest: RestClient
): Promise<OobResult> {
const method = parameters.get("outofbandtype"); const method = parameters.get("outofbandtype");
if (method == null) { if (method == null) {
throw new Error("Out of band method is not specified"); throw new Error("Out of band method is not specified");
@@ -335,12 +340,12 @@ export class Client {
} }
} }
private approveDuo( private async approveDuo(
username: string, username: string,
parameters: Map<string, string>, parameters: Map<string, string>,
ui: Ui, ui: Ui,
rest: RestClient rest: RestClient
): OobResult { ): Promise<OobResult> {
return parameters.get("preferduowebsdk") == "1" return parameters.get("preferduowebsdk") == "1"
? this.approveDuoWebSdk(username, parameters, ui, rest) ? this.approveDuoWebSdk(username, parameters, ui, rest)
: ui.approveDuo(); : ui.approveDuo();
@@ -525,6 +530,7 @@ export class Client {
switch (cause.value) { switch (cause.value) {
case "unknownemail": case "unknownemail":
return "Invalid username"; return "Invalid username";
case "password_invalid":
case "unknownpassword": case "unknownpassword":
return "Invalid password"; return "Invalid password";
case "googleauthfailed": case "googleauthfailed":

View File

@@ -43,9 +43,6 @@ export class RestClient {
): Promise<Response> { ): Promise<Response> {
const setBody = (requestInit: RequestInit, headerMap: Map<string, string>) => { const setBody = (requestInit: RequestInit, headerMap: Map<string, string>) => {
if (body != null) { if (body != null) {
if (headerMap == null) {
headerMap = new Map<string, string>();
}
headerMap.set("Content-Type", "application/json; charset=utf-8"); headerMap.set("Content-Type", "application/json; charset=utf-8");
requestInit.body = JSON.stringify(body); requestInit.body = JSON.stringify(body);
} }
@@ -63,6 +60,9 @@ export class RestClient {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
}; };
if (headers == null) {
headers = new Map<string, string>();
}
setBody(requestInit, headers); setBody(requestInit, headers);
this.setHeaders(requestInit, headers, cookies); this.setHeaders(requestInit, headers, cookies);
const request = new Request(this.baseUrl + "/" + endpoint, requestInit); const request = new Request(this.baseUrl + "/" + endpoint, requestInit);

View File

@@ -4,9 +4,9 @@ import { DuoUi } from "./duo-ui";
export abstract class Ui extends DuoUi { export abstract class Ui extends DuoUi {
// To cancel return OtpResult.Cancel, otherwise only valid data is expected. // To cancel return OtpResult.Cancel, otherwise only valid data is expected.
provideGoogleAuthPasscode: () => OtpResult; provideGoogleAuthPasscode: () => Promise<OtpResult>;
provideMicrosoftAuthPasscode: () => OtpResult; provideMicrosoftAuthPasscode: () => Promise<OtpResult>;
provideYubikeyPasscode: () => OtpResult; provideYubikeyPasscode: () => Promise<OtpResult>;
/* /*
The UI implementations should provide the following possibilities for the user: The UI implementations should provide the following possibilities for the user:
@@ -23,7 +23,7 @@ export abstract class Ui extends DuoUi {
passcode instead of performing an action in the app. In this case the UI should return passcode instead of performing an action in the app. In this case the UI should return
OobResult.continueWithPasscode(passcode, rememberMe). OobResult.continueWithPasscode(passcode, rememberMe).
*/ */
approveLastPassAuth: () => OobResult; approveLastPassAuth: () => Promise<OobResult>;
approveDuo: () => OobResult; approveDuo: () => Promise<OobResult>;
approveSalesforceAuth: () => OobResult; approveSalesforceAuth: () => Promise<OobResult>;
} }

View File

@@ -1,3 +1,5 @@
import * as papa from "papaparse";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { HttpStatusCode } from "@bitwarden/common/enums"; import { HttpStatusCode } from "@bitwarden/common/enums";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@@ -7,6 +9,7 @@ import { IdpProvider } from "./enums";
import { import {
Account, Account,
ClientInfo, ClientInfo,
ExportedAccount,
FederatedUserContext, FederatedUserContext,
ParserOptions, ParserOptions,
UserTypeContext, UserTypeContext,
@@ -68,20 +71,35 @@ export class Vault {
if (response.status === HttpStatusCode.Ok) { if (response.status === HttpStatusCode.Ok) {
const json = await response.json(); const json = await response.json();
this.userType = new UserTypeContext(); this.userType = new UserTypeContext();
this.userType.CompanyId = json.CompanyId; this.userType.companyId = json.CompanyId;
this.userType.IdentityProviderGUID = json.IdentityProviderGUID; this.userType.identityProviderGUID = json.IdentityProviderGUID;
this.userType.IdentityProviderURL = json.IdentityProviderURL; this.userType.identityProviderURL = json.IdentityProviderURL;
this.userType.IsPasswordlessEnabled = json.IsPasswordlessEnabled; this.userType.isPasswordlessEnabled = json.IsPasswordlessEnabled;
this.userType.OpenIDConnectAuthority = json.OpenIDConnectAuthority; this.userType.openIDConnectAuthority = json.OpenIDConnectAuthority;
this.userType.OpenIDConnectClientId = json.OpenIDConnectClientId; this.userType.openIDConnectClientId = json.OpenIDConnectClientId;
this.userType.PkceEnabled = json.PkceEnabled; this.userType.pkceEnabled = json.PkceEnabled;
this.userType.Provider = json.Provider; this.userType.provider = json.Provider;
this.userType.type = json.type; this.userType.type = json.type;
return; return;
} }
throw new Error("Cannot determine LastPass user type."); throw new Error("Cannot determine LastPass user type.");
} }
accountsToExportedCsvString(skipShared = false): string {
if (this.accounts == null) {
throw new Error("Vault has not opened any accounts.");
}
const exportedAccounts = this.accounts
.filter((a) => !a.isShared || (a.isShared && !skipShared))
.map((a) => new ExportedAccount(a));
if (exportedAccounts.length === 0) {
throw new Error("No accounts to transform");
}
return papa.unparse(exportedAccounts);
}
private async getK1(federatedUser: FederatedUserContext): Promise<Uint8Array> { private async getK1(federatedUser: FederatedUserContext): Promise<Uint8Array> {
if (this.userType == null) { if (this.userType == null) {
throw new Error("User type is not set."); throw new Error("User type is not set.");
@@ -96,18 +114,18 @@ export class Vault {
} }
let k1: Uint8Array = null; let k1: Uint8Array = null;
if (federatedUser.idpUserInfo?.LastPassK1 !== null) { if (federatedUser.idpUserInfo?.LastPassK1 != null) {
return Utils.fromByteStringToArray(federatedUser.idpUserInfo.LastPassK1); return Utils.fromByteStringToArray(federatedUser.idpUserInfo.LastPassK1);
} else if (this.userType.Provider === IdpProvider.Azure) { } else if (this.userType.provider === IdpProvider.Azure) {
k1 = await this.getK1Azure(federatedUser); k1 = await this.getK1Azure(federatedUser);
} else if (this.userType.Provider === IdpProvider.Google) { } else if (this.userType.provider === IdpProvider.Google) {
k1 = await this.getK1Google(federatedUser); k1 = await this.getK1Google(federatedUser);
} else { } else {
const b64Encoded = this.userType.Provider === IdpProvider.PingOne; const b64Encoded = this.userType.provider === IdpProvider.PingOne;
k1 = this.getK1FromAccessToken(federatedUser, b64Encoded); k1 = await this.getK1FromAccessToken(federatedUser, b64Encoded);
} }
if (k1 !== null) { if (k1 != null) {
return k1; return k1;
} }
@@ -125,7 +143,7 @@ export class Vault {
if (response.status === HttpStatusCode.Ok) { if (response.status === HttpStatusCode.Ok) {
const json = await response.json(); const json = await response.json();
const k1 = json?.extensions?.LastPassK1 as string; const k1 = json?.extensions?.LastPassK1 as string;
if (k1 !== null) { if (k1 != null) {
return Utils.fromB64ToArray(k1); return Utils.fromB64ToArray(k1);
} }
} }
@@ -149,7 +167,7 @@ export class Vault {
if (response.status === HttpStatusCode.Ok) { if (response.status === HttpStatusCode.Ok) {
const json = await response.json(); const json = await response.json();
const files = json?.files as any[]; const files = json?.files as any[];
if (files !== null && files.length > 0 && files[0].id != null && files[0].name === "k1.lp") { if (files != null && files.length > 0 && files[0].id != null && files[0].name === "k1.lp") {
// Open the k1.lp file // Open the k1.lp file
rest.baseUrl = "https://www.googleapis.com"; rest.baseUrl = "https://www.googleapis.com";
const response = await rest.get( const response = await rest.get(
@@ -165,10 +183,10 @@ export class Vault {
return null; return null;
} }
private getK1FromAccessToken(federatedUser: FederatedUserContext, b64: boolean) { private async getK1FromAccessToken(federatedUser: FederatedUserContext, b64: boolean) {
const decodedAccessToken = this.tokenService.decodeToken(federatedUser.accessToken); const decodedAccessToken = await this.tokenService.decodeToken(federatedUser.accessToken);
const k1 = decodedAccessToken?.LastPassK1 as string; const k1 = decodedAccessToken?.LastPassK1 as string;
if (k1 !== null) { if (k1 != null) {
return b64 ? Utils.fromB64ToArray(k1) : Utils.fromByteStringToArray(k1); return b64 ? Utils.fromB64ToArray(k1) : Utils.fromByteStringToArray(k1);
} }
return null; return null;
@@ -184,15 +202,15 @@ export class Vault {
} }
const rest = new RestClient(); const rest = new RestClient();
rest.baseUrl = this.userType.IdentityProviderURL; rest.baseUrl = this.userType.identityProviderURL;
const response = await rest.postJson("federatedlogin/api/v1/getkey", { const response = await rest.postJson("federatedlogin/api/v1/getkey", {
company_id: this.userType.CompanyId, company_id: this.userType.companyId,
id_token: federatedUser.idToken, id_token: federatedUser.idToken,
}); });
if (response.status === HttpStatusCode.Ok) { if (response.status === HttpStatusCode.Ok) {
const json = await response.json(); const json = await response.json();
const k2 = json?.k2 as string; const k2 = json?.k2 as string;
if (k2 !== null) { if (k2 != null) {
return Utils.fromB64ToArray(k2); return Utils.fromB64ToArray(k2);
} }
} }