mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
* Move cryptofunction service to km * Fix formatting * Fix import * Fix build on desktop * Fix build on browser and tests
250 lines
8.2 KiB
TypeScript
250 lines
8.2 KiB
TypeScript
// FIXME: Update this file to be type safe and remove this and next line
|
|
// @ts-strict-ignore
|
|
import * as papa from "papaparse";
|
|
|
|
import { decodeJwtTokenToJson } from "@bitwarden/auth/common";
|
|
import { HttpStatusCode } from "@bitwarden/common/enums";
|
|
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
|
|
import { IdpProvider } from "./enums";
|
|
import {
|
|
Account,
|
|
ClientInfo,
|
|
ExportedAccount,
|
|
FederatedUserContext,
|
|
ParserOptions,
|
|
UserTypeContext,
|
|
} from "./models";
|
|
import { Client, CryptoUtils, Parser, RestClient } from "./services";
|
|
import { Ui } from "./ui";
|
|
|
|
export class Vault {
|
|
accounts: Account[];
|
|
userType: UserTypeContext;
|
|
|
|
private client: Client;
|
|
private cryptoUtils: CryptoUtils;
|
|
|
|
constructor(private cryptoFunctionService: CryptoFunctionService) {
|
|
this.cryptoUtils = new CryptoUtils(cryptoFunctionService);
|
|
const parser = new Parser(cryptoFunctionService, this.cryptoUtils);
|
|
this.client = new Client(parser, this.cryptoUtils);
|
|
}
|
|
|
|
async open(
|
|
username: string,
|
|
password: string,
|
|
clientInfo: ClientInfo,
|
|
ui: Ui,
|
|
parserOptions: ParserOptions = ParserOptions.default,
|
|
): Promise<void> {
|
|
this.accounts = await this.client.openVault(
|
|
username,
|
|
password,
|
|
null,
|
|
clientInfo,
|
|
ui,
|
|
parserOptions,
|
|
);
|
|
}
|
|
|
|
async openFederated(
|
|
federatedUser: FederatedUserContext,
|
|
clientInfo: ClientInfo,
|
|
ui: Ui,
|
|
parserOptions: ParserOptions = ParserOptions.default,
|
|
): Promise<void> {
|
|
if (federatedUser == null) {
|
|
throw new Error("Federated user context is not set.");
|
|
}
|
|
const k1 = await this.getK1(federatedUser);
|
|
const [k2, fragmentId] = await this.getK2FragmentId(federatedUser);
|
|
const hiddenPasswordArr = await this.cryptoFunctionService.hash(
|
|
this.cryptoUtils.ExclusiveOr(k1, k2),
|
|
"sha256",
|
|
);
|
|
const hiddenPassword = Utils.fromBufferToB64(hiddenPasswordArr);
|
|
this.accounts = await this.client.openVault(
|
|
federatedUser.username,
|
|
hiddenPassword,
|
|
fragmentId,
|
|
clientInfo,
|
|
ui,
|
|
parserOptions,
|
|
);
|
|
}
|
|
|
|
async setUserTypeContext(username: string) {
|
|
const lowercaseUsername = username.toLowerCase();
|
|
const rest = new RestClient();
|
|
rest.baseUrl = "https://lastpass.com";
|
|
const endpoint = "lmiapi/login/type?username=" + encodeURIComponent(lowercaseUsername);
|
|
const response = await rest.get(endpoint);
|
|
if (response.status === HttpStatusCode.Ok) {
|
|
const json = await response.json();
|
|
this.userType = new UserTypeContext();
|
|
this.userType.companyId = json.CompanyId;
|
|
this.userType.identityProviderGUID = json.IdentityProviderGUID;
|
|
this.userType.identityProviderURL = json.IdentityProviderURL;
|
|
this.userType.isPasswordlessEnabled = json.IsPasswordlessEnabled;
|
|
this.userType.openIDConnectAuthority = json.OpenIDConnectAuthority;
|
|
this.userType.openIDConnectClientId = json.OpenIDConnectClientId;
|
|
this.userType.pkceEnabled = json.PkceEnabled;
|
|
this.userType.provider = json.Provider;
|
|
this.userType.type = json.type;
|
|
|
|
if (this.userType.provider === IdpProvider.Azure) {
|
|
// Sometimes customers have malformed OIDC authority URLs. Try to fix them.
|
|
const appQueryIndex = this.userType.openIDConnectAuthority.indexOf("?app");
|
|
if (appQueryIndex > -1) {
|
|
this.userType.openIDConnectAuthority = this.userType.openIDConnectAuthority.substring(
|
|
0,
|
|
appQueryIndex,
|
|
);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
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> {
|
|
if (this.userType == null) {
|
|
throw new Error("User type is not set.");
|
|
}
|
|
|
|
if (!this.userType.isFederated()) {
|
|
throw new Error("Cannot get k1 for LastPass user that is not federated.");
|
|
}
|
|
|
|
if (federatedUser == null) {
|
|
throw new Error("Federated user is not set.");
|
|
}
|
|
|
|
let k1: Uint8Array = null;
|
|
if (federatedUser.idpUserInfo?.LastPassK1 != null) {
|
|
return Utils.fromByteStringToArray(federatedUser.idpUserInfo.LastPassK1);
|
|
} else if (this.userType.provider === IdpProvider.Azure) {
|
|
k1 = await this.getK1Azure(federatedUser);
|
|
} else if (this.userType.provider === IdpProvider.Google) {
|
|
k1 = await this.getK1Google(federatedUser);
|
|
} else {
|
|
const b64Encoded = this.userType.provider === IdpProvider.PingOne;
|
|
k1 = await this.getK1FromAccessToken(federatedUser, b64Encoded);
|
|
}
|
|
|
|
if (k1 != null) {
|
|
return k1;
|
|
}
|
|
|
|
throw new Error("Cannot get k1.");
|
|
}
|
|
|
|
private async getK1Azure(federatedUser: FederatedUserContext) {
|
|
// Query the Graph API for the k1 field
|
|
const rest = new RestClient();
|
|
rest.baseUrl = "https://graph.microsoft.com";
|
|
const response = await rest.get(
|
|
"v1.0/me?$select=id,displayName,mail&$expand=extensions",
|
|
new Map([["Authorization", "Bearer " + federatedUser.accessToken]]),
|
|
);
|
|
if (response.status === HttpStatusCode.Ok) {
|
|
const json = await response.json();
|
|
if (json?.extensions != null && json.extensions.length > 0) {
|
|
const k1 = json.extensions[0].LastPassK1 as string;
|
|
if (k1 != null) {
|
|
return Utils.fromB64ToArray(k1);
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private async getK1Google(federatedUser: FederatedUserContext) {
|
|
// Query Google Drive for the k1.lp file
|
|
const accessTokenAuthHeader = new Map([
|
|
["Authorization", "Bearer " + federatedUser.accessToken],
|
|
]);
|
|
const rest = new RestClient();
|
|
rest.baseUrl = "https://content.googleapis.com";
|
|
const response = await rest.get(
|
|
"drive/v3/files?pageSize=1" +
|
|
"&q=name%20%3D%20%27k1.lp%27" +
|
|
"&spaces=appDataFolder" +
|
|
"&fields=nextPageToken%2C%20files(id%2C%20name)",
|
|
accessTokenAuthHeader,
|
|
);
|
|
if (response.status === HttpStatusCode.Ok) {
|
|
const json = await response.json();
|
|
const files = json?.files as any[];
|
|
if (files != null && files.length > 0 && files[0].id != null && files[0].name === "k1.lp") {
|
|
// Open the k1.lp file
|
|
rest.baseUrl = "https://www.googleapis.com";
|
|
const response = await rest.get(
|
|
"drive/v3/files/" + files[0].id + "?alt=media",
|
|
accessTokenAuthHeader,
|
|
);
|
|
if (response.status === HttpStatusCode.Ok) {
|
|
const k1 = await response.text();
|
|
return Utils.fromB64ToArray(k1);
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private async getK1FromAccessToken(federatedUser: FederatedUserContext, b64: boolean) {
|
|
const decodedAccessToken = decodeJwtTokenToJson(federatedUser.accessToken);
|
|
const k1 = decodedAccessToken?.LastPassK1 as string;
|
|
if (k1 != null) {
|
|
return b64 ? Utils.fromB64ToArray(k1) : Utils.fromByteStringToArray(k1);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private async getK2FragmentId(
|
|
federatedUser: FederatedUserContext,
|
|
): Promise<[Uint8Array, string]> {
|
|
if (this.userType == null) {
|
|
throw new Error("User type is not set.");
|
|
}
|
|
|
|
if (!this.userType.isFederated()) {
|
|
throw new Error("Cannot get k2 for LastPass user that is not federated.");
|
|
}
|
|
|
|
const rest = new RestClient();
|
|
rest.baseUrl = this.userType.identityProviderURL;
|
|
const response = await rest.postJson("federatedlogin/api/v1/getkey", {
|
|
company_id: this.userType.companyId,
|
|
id_token: federatedUser.idToken,
|
|
});
|
|
if (response.status === HttpStatusCode.Ok) {
|
|
const json = await response.json();
|
|
const k2 = json?.k2 as string;
|
|
const fragmentId = json?.fragment_id as string;
|
|
if (k2 != null && fragmentId != null) {
|
|
return [Utils.fromB64ToArray(k2), fragmentId];
|
|
}
|
|
}
|
|
throw new Error("Cannot get k2 and/or fragment ID.");
|
|
}
|
|
}
|