1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-10-03 04:59:06 +02:00

[PM-4195] Lastpass access lib refactors (#6566)

* Fix setUserTypeContext by adding missing return

* Throw new Error instead of just string

* Move enums and models into separate folders

* Move UI classes into separate folder

* Move FederatedUserContext import

* Move services into a separate folder

* Add barrel file for lastpass access lib

* Fix build by updating imports after move

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
Daniel James Smith 2023-10-13 10:41:47 +02:00 committed by GitHub
parent b592b71df1
commit 67fa1e06d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 112 additions and 85 deletions

View File

@ -0,0 +1,6 @@
export enum DuoFactor {
Push,
Call,
Passcode,
SendPasscodesBySms,
}

View File

@ -0,0 +1,5 @@
export enum DuoStatus {
Success,
Error,
Info,
}

View File

@ -0,0 +1,8 @@
export enum IdpProvider {
Azure = 0,
OktaAuthServer = 1,
OktaNoAuthServer = 2,
Google = 3,
PingOne = 4,
OneLogin = 5,
}

View File

@ -0,0 +1,6 @@
export { DuoFactor } from "./duo-factor";
export { DuoStatus } from "./duo-status";
export { IdpProvider } from "./idp-provider";
export { LastpassLoginType } from "./lastpass-login-type";
export { OtpMethod } from "./otp-method";
export { Platform } from "./platform";

View File

@ -0,0 +1,5 @@
export enum LastpassLoginType {
MasterPassword = 0,
// Not sure what Types 1 and 2 are?
Federated = 3,
}

View File

@ -0,0 +1 @@
export { Vault } from "./vault";

View File

@ -1,4 +1,4 @@
import { Platform } from "./platform";
import { Platform } from "../enums";
export class ClientInfo {
platform: Platform;

View File

@ -0,0 +1,10 @@
export { Account } from "./account";
export { Chunk } from "./chunk";
export { ClientInfo } from "./client-info";
export { FederatedUserContext } from "./federated-user-context";
export { OobResult } from "./oob-result";
export { OtpResult } from "./otp-result";
export { ParserOptions } from "./parser-options";
export { Session } from "./session";
export { SharedFolder } from "./shared-folder";
export { UserTypeContext } from "./user-type-context";

View File

@ -1,4 +1,4 @@
import { Platform } from "./platform";
import { Platform } from "../enums";
export class Session {
id: string;

View File

@ -1,17 +1,19 @@
import { IdpProvider, LastpassLoginType } from "../enums";
export class UserTypeContext {
type: Type;
type: LastpassLoginType;
IdentityProviderGUID: string;
IdentityProviderURL: string;
OpenIDConnectAuthority: string;
OpenIDConnectClientId: string;
CompanyId: number;
Provider: Provider;
Provider: IdpProvider;
PkceEnabled: boolean;
IsPasswordlessEnabled: boolean;
isFederated(): boolean {
return (
this.type === Type.Federated &&
this.type === LastpassLoginType.Federated &&
this.hasValue(this.IdentityProviderURL) &&
this.hasValue(this.OpenIDConnectAuthority) &&
this.hasValue(this.OpenIDConnectClientId)
@ -22,18 +24,3 @@ export class UserTypeContext {
return str != null && str.trim() !== "";
}
}
export enum Provider {
Azure = 0,
OktaAuthServer = 1,
OktaNoAuthServer = 2,
Google = 3,
PingOne = 4,
OneLogin = 5,
}
export enum Type {
MasterPassword = 0,
// Not sure what Types 1 and 2 are?
Federated = 3,
}

View File

@ -12,7 +12,7 @@ export class BinaryReader {
readBytes(count: number): Uint8Array {
if (this.position + count > this.arr.length) {
throw "End of array reached";
throw new Error("End of array reached");
}
const slice = this.arr.subarray(this.position, this.position + count);
this.position += count;
@ -62,10 +62,10 @@ export class BinaryReader {
seekFromCurrentPosition(offset: number) {
const newPosition = this.position + offset;
if (newPosition < 0) {
throw "Position cannot be negative";
throw new Error("Position cannot be negative");
}
if (newPosition > this.arr.length) {
throw "Array not large enough to seek to this position";
throw new Error("Array not large enough to seek to this position");
}
this.position = newPosition;
}

View File

@ -1,21 +1,23 @@
import { HttpStatusCode } from "@bitwarden/common/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { Account } from "./account";
import { OtpMethod, Platform } from "../enums";
import {
Account,
Chunk,
ClientInfo,
OobResult,
OtpResult,
ParserOptions,
Session,
SharedFolder,
} from "../models";
import { Ui } from "../ui";
import { BinaryReader } from "./binary-reader";
import { Chunk } from "./chunk";
import { ClientInfo } from "./client-info";
import { CryptoUtils } from "./crypto-utils";
import { OobResult } from "./oob-result";
import { OtpMethod } from "./otp-method";
import { OtpResult } from "./otp-result";
import { Parser } from "./parser";
import { ParserOptions } from "./parser-options";
import { Platform } from "./platform";
import { RestClient } from "./rest-client";
import { Session } from "./session";
import { SharedFolder } from "./shared-folder";
import { Ui } from "./ui";
const PlatformToUserAgent = new Map<Platform, string>([
[Platform.Desktop, "cli"],
@ -68,7 +70,7 @@ export class Client {
const reader = new BinaryReader(blob);
const chunks = this.parser.extractChunks(reader);
if (!this.isComplete(chunks)) {
throw "Blob is truncated or corrupted";
throw new Error("Blob is truncated or corrupted");
}
return await this.parseAccounts(chunks, encryptionKey, privateKey, options);
}
@ -236,11 +238,11 @@ export class Client {
passcode = ui.provideYubikeyPasscode();
break;
default:
throw "Invalid OTP method";
throw new Error("Invalid OTP method");
}
if (passcode == OtpResult.cancel) {
throw "Second factor step is canceled by the user";
throw new Error("Second factor step is canceled by the user");
}
const response = await this.performSingleLoginRequest(
@ -273,7 +275,7 @@ export class Client {
): Promise<Session> {
const answer = this.approveOob(username, parameters, ui, rest);
if (answer == OobResult.cancel) {
throw "Out of band step is canceled by the user";
throw new Error("Out of band step is canceled by the user");
}
const extraParameters = new Map<string, any>();
@ -319,7 +321,7 @@ export class Client {
private approveOob(username: string, parameters: Map<string, string>, ui: Ui, rest: RestClient) {
const method = parameters.get("outofbandtype");
if (method == null) {
throw "Out of band method is not specified";
throw new Error("Out of band method is not specified");
}
switch (method) {
case "lastpassauth":
@ -329,7 +331,7 @@ export class Client {
case "salesforcehash":
return ui.approveSalesforceAuth();
default:
throw "Out of band method " + method + " is not supported";
throw new Error("Out of band method " + method + " is not supported");
}
}
@ -410,7 +412,7 @@ export class Client {
if (attr != null) {
return attr;
}
throw "Unknown response schema: attribute " + name + " is missing";
throw new Error("Unknown response schema: attribute " + name + " is missing");
}
private getOptionalErrorAttribute(response: Document, name: string): string {
@ -505,7 +507,9 @@ export class Client {
private makeError(response: Response) {
// TODO: error parsing
throw "HTTP request to " + response.url + " failed with status " + response.status + ".";
throw new Error(
"HTTP request to " + response.url + " failed with status " + response.status + "."
);
}
private makeLoginError(response: Document): string {

View File

@ -6,7 +6,7 @@ export class CryptoUtils {
async deriveKey(username: string, password: string, iterationCount: number) {
if (iterationCount < 0) {
throw "Iteration count should be positive";
throw new Error("Iteration count should be positive");
}
if (iterationCount == 1) {
return await this.cryptoFunctionService.hash(username + password, "sha256");
@ -27,7 +27,7 @@ export class CryptoUtils {
ExclusiveOr(arr1: Uint8Array, arr2: Uint8Array) {
if (arr1.length !== arr2.length) {
throw "Arrays must be the same length.";
throw new Error("Arrays must be the same length.");
}
const result = new Uint8Array(arr1.length);
for (let i = 0; i < arr1.length; i++) {

View File

@ -0,0 +1,5 @@
export { BinaryReader } from "./binary-reader";
export { Client } from "./client";
export { CryptoUtils } from "./crypto-utils";
export { Parser } from "./parser";
export { RestClient } from "./rest-client";

View File

@ -1,12 +1,10 @@
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { Account } from "./account";
import { Account, Chunk, ParserOptions, SharedFolder } from "../models";
import { BinaryReader } from "./binary-reader";
import { Chunk } from "./chunk";
import { CryptoUtils } from "./crypto-utils";
import { ParserOptions } from "./parser-options";
import { SharedFolder } from "./shared-folder";
const AllowedSecureNoteTypes = new Set<string>([
"Server",
@ -285,7 +283,7 @@ export class Parser {
const header = "LastPassPrivateKey<";
const footer = ">LastPassPrivateKey";
if (!decrypted.startsWith(header) || !decrypted.endsWith(footer)) {
throw "Failed to decrypt private key";
throw new Error("Failed to decrypt private key");
}
const parsedKey = decrypted.substring(header.length, decrypted.length - footer.length);

View File

@ -1,3 +1,5 @@
import { DuoFactor, DuoStatus } from "../enums";
// Adds Duo functionality to the module-specific Ui class.
export abstract class DuoUi {
// To cancel return null
@ -8,19 +10,6 @@ export abstract class DuoUi {
updateDuoStatus: (status: DuoStatus, text: string) => void;
}
export enum DuoFactor {
Push,
Call,
Passcode,
SendPasscodesBySms,
}
export enum DuoStatus {
Success,
Error,
Info,
}
export interface DuoChoice {
device: DuoDevice;
factor: DuoFactor;

View File

@ -0,0 +1,2 @@
export { DuoUi, DuoChoice, DuoDevice } from "./duo-ui";
export { Ui } from "./ui";

View File

@ -1,6 +1,6 @@
import { OobResult, OtpResult } from "../models";
import { DuoUi } from "./duo-ui";
import { OobResult } from "./oob-result";
import { OtpResult } from "./otp-result";
export abstract class Ui extends DuoUi {
// To cancel return OtpResult.Cancel, otherwise only valid data is expected.

View File

@ -3,16 +3,16 @@ import { HttpStatusCode } from "@bitwarden/common/enums";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { Account } from "./account";
import { Client } from "./client";
import { ClientInfo } from "./client-info";
import { CryptoUtils } from "./crypto-utils";
import { FederatedUserContext } from "./federated-user-context";
import { Parser } from "./parser";
import { ParserOptions } from "./parser-options";
import { RestClient } from "./rest-client";
import { IdpProvider } from "./enums";
import {
Account,
ClientInfo,
FederatedUserContext,
ParserOptions,
UserTypeContext,
} from "./models";
import { Client, CryptoUtils, Parser, RestClient } from "./services";
import { Ui } from "./ui";
import { Provider, UserTypeContext } from "./user-type-context";
export class Vault {
accounts: Account[];
@ -47,7 +47,7 @@ export class Vault {
parserOptions: ParserOptions = ParserOptions.default
): Promise<void> {
if (federatedUser == null) {
throw "Federated user context is not set.";
throw new Error("Federated user context is not set.");
}
const k1 = await this.getK1(federatedUser);
const k2 = await this.getK2(federatedUser);
@ -77,32 +77,33 @@ export class Vault {
this.userType.PkceEnabled = json.PkceEnabled;
this.userType.Provider = json.Provider;
this.userType.type = json.type;
return;
}
throw "Cannot determine LastPass user type.";
throw new Error("Cannot determine LastPass user type.");
}
private async getK1(federatedUser: FederatedUserContext): Promise<Uint8Array> {
if (this.userType == null) {
throw "User type is not set.";
throw new Error("User type is not set.");
}
if (!this.userType.isFederated()) {
throw "Cannot get k1 for LastPass user that is not federated.";
throw new Error("Cannot get k1 for LastPass user that is not federated.");
}
if (federatedUser == null) {
throw "Federated user is not set.";
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 === Provider.Azure) {
} else if (this.userType.Provider === IdpProvider.Azure) {
k1 = await this.getK1Azure(federatedUser);
} else if (this.userType.Provider === Provider.Google) {
} else if (this.userType.Provider === IdpProvider.Google) {
k1 = await this.getK1Google(federatedUser);
} else {
const b64Encoded = this.userType.Provider === Provider.PingOne;
const b64Encoded = this.userType.Provider === IdpProvider.PingOne;
k1 = this.getK1FromAccessToken(federatedUser, b64Encoded);
}
@ -110,7 +111,7 @@ export class Vault {
return k1;
}
throw "Cannot get k1.";
throw new Error("Cannot get k1.");
}
private async getK1Azure(federatedUser: FederatedUserContext) {
@ -175,11 +176,11 @@ export class Vault {
private async getK2(federatedUser: FederatedUserContext): Promise<Uint8Array> {
if (this.userType == null) {
throw "User type is not set.";
throw new Error("User type is not set.");
}
if (!this.userType.isFederated()) {
throw "Cannot get k2 for LastPass user that is not federated.";
throw new Error("Cannot get k2 for LastPass user that is not federated.");
}
const rest = new RestClient();
@ -195,6 +196,6 @@ export class Vault {
return Utils.fromB64ToArray(k2);
}
}
throw "Cannot get k2.";
throw new Error("Cannot get k2.");
}
}