This commit is contained in:
Kyle Spearrin 2024-01-23 13:21:59 -05:00 committed by GitHub
parent 014281cb93
commit e359aef979
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 63 additions and 9 deletions

View File

@ -39,12 +39,19 @@ export class Client {
async openVault(
username: string,
password: string,
fragmentId: string,
clientInfo: ClientInfo,
ui: Ui,
options: ParserOptions,
): Promise<Account[]> {
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<string, any>(),
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<string, string>([["otp", passcode.passcode]]),
clientInfo,
@ -270,6 +283,7 @@ export class Client {
private async loginWithOob(
username: string,
password: string,
fragmentId: string,
keyIterationCount: number,
parameters: Map<string, string>,
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<string, any>,
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);
}

View File

@ -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);
}
}

View File

@ -40,7 +40,14 @@ export class Vault {
ui: Ui,
parserOptions: ParserOptions = ParserOptions.default,
): Promise<void> {
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<Uint8Array> {
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.");
}
}