diff --git a/libs/importer/src/importers/lastpass/access/services/client.ts b/libs/importer/src/importers/lastpass/access/services/client.ts index 341bbc59ce..dc0ce51b93 100644 --- a/libs/importer/src/importers/lastpass/access/services/client.ts +++ b/libs/importer/src/importers/lastpass/access/services/client.ts @@ -39,12 +39,19 @@ export class Client { async openVault( username: string, password: string, + fragmentId: string, clientInfo: ClientInfo, ui: Ui, options: ParserOptions, ): Promise { const lowercaseUsername = username.toLowerCase(); - const [session, rest] = await this.login(lowercaseUsername, password, clientInfo, ui); + const [session, rest] = await this.login( + lowercaseUsername, + password, + fragmentId, + clientInfo, + ui, + ); try { const blob = await this.downloadVault(session, rest); const key = await this.cryptoUtils.deriveKey( @@ -111,6 +118,7 @@ export class Client { private async login( username: string, password: string, + fragmentId: string, clientInfo: ClientInfo, ui: Ui, ): Promise<[Session, RestClient]> { @@ -142,6 +150,7 @@ export class Client { response = await this.performSingleLoginRequest( username, password, + fragmentId, keyIterationCount, new Map(), clientInfo, @@ -191,6 +200,7 @@ export class Client { session = await this.loginWithOtp( username, password, + fragmentId, keyIterationCount, optMethod, clientInfo, @@ -203,6 +213,7 @@ export class Client { session = await this.loginWithOob( username, password, + fragmentId, keyIterationCount, this.getAllErrorAttributes(response), clientInfo, @@ -223,6 +234,7 @@ export class Client { private async loginWithOtp( username: string, password: string, + fragmentId: string, keyIterationCount: number, method: OtpMethod, clientInfo: ClientInfo, @@ -251,6 +263,7 @@ export class Client { const response = await this.performSingleLoginRequest( username, password, + fragmentId, keyIterationCount, new Map([["otp", passcode.passcode]]), clientInfo, @@ -270,6 +283,7 @@ export class Client { private async loginWithOob( username: string, password: string, + fragmentId: string, keyIterationCount: number, parameters: Map, clientInfo: ClientInfo, @@ -282,6 +296,7 @@ export class Client { const response = await this.performSingleLoginRequest( username, password, + fragmentId, keyIterationCount, extraParameters, clientInfo, @@ -494,6 +509,7 @@ export class Client { private async performSingleLoginRequest( username: string, password: string, + fragmentId: string, keyIterationCount: number, extraParameters: Map, clientInfo: ClientInfo, @@ -513,6 +529,10 @@ export class Client { // TODO: Test against the real server if it's ok to send this every time! ["trustlabel", clientInfo.description], ]); + if (fragmentId != null) { + parameters.set("alpfragmentid", fragmentId); + parameters.set("calculatedfragmentid", fragmentId); + } for (const [key, value] of extraParameters) { parameters.set(key, value); } diff --git a/libs/importer/src/importers/lastpass/access/services/parser.ts b/libs/importer/src/importers/lastpass/access/services/parser.ts index 41fa7dfd34..abf55172c8 100644 --- a/libs/importer/src/importers/lastpass/access/services/parser.ts +++ b/libs/importer/src/importers/lastpass/access/services/parser.ts @@ -55,7 +55,7 @@ export class Parser { // 3: url let url = Utils.fromBufferToUtf8( - Utils.fromHexToArray(Utils.fromBufferToUtf8(this.readItem(reader))), + this.decodeHexLoose(Utils.fromBufferToUtf8(this.readItem(reader))), ); // Ignore "group" accounts. They have no credentials. @@ -354,4 +354,9 @@ export class Parser { private readPayload(reader: BinaryReader, size: number): Uint8Array { return reader.readBytes(size); } + + private decodeHexLoose(s: string): Uint8Array { + // This is a forgiving version that pads the input with a '0' when the length is odd + return Utils.fromHexToArray(s.length % 2 == 0 ? s : "0" + s); + } } diff --git a/libs/importer/src/importers/lastpass/access/vault.ts b/libs/importer/src/importers/lastpass/access/vault.ts index f6e0f9f167..814390f5c8 100644 --- a/libs/importer/src/importers/lastpass/access/vault.ts +++ b/libs/importer/src/importers/lastpass/access/vault.ts @@ -40,7 +40,14 @@ export class Vault { ui: Ui, parserOptions: ParserOptions = ParserOptions.default, ): Promise { - this.accounts = await this.client.openVault(username, password, clientInfo, ui, parserOptions); + this.accounts = await this.client.openVault( + username, + password, + null, + clientInfo, + ui, + parserOptions, + ); } async openFederated( @@ -53,13 +60,20 @@ export class Vault { throw new Error("Federated user context is not set."); } const k1 = await this.getK1(federatedUser); - const k2 = await this.getK2(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); - await this.open(federatedUser.username, hiddenPassword, clientInfo, ui, parserOptions); + this.accounts = await this.client.openVault( + federatedUser.username, + hiddenPassword, + fragmentId, + clientInfo, + ui, + parserOptions, + ); } async setUserTypeContext(username: string) { @@ -80,6 +94,18 @@ export class Vault { 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."); @@ -194,7 +220,9 @@ export class Vault { return null; } - private async getK2(federatedUser: FederatedUserContext): Promise { + private async getK2FragmentId( + federatedUser: FederatedUserContext, + ): Promise<[Uint8Array, string]> { if (this.userType == null) { throw new Error("User type is not set."); } @@ -212,10 +240,11 @@ export class Vault { if (response.status === HttpStatusCode.Ok) { const json = await response.json(); const k2 = json?.k2 as string; - if (k2 != null) { - return Utils.fromB64ToArray(k2); + const fragmentId = json?.fragment_id as string; + if (k2 != null && fragmentId != null) { + return [Utils.fromB64ToArray(k2), fragmentId]; } } - throw new Error("Cannot get k2."); + throw new Error("Cannot get k2 and/or fragment ID."); } }