mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-13 13:49:37 +01:00
[PM-3758] Handle user decryption options from pre-TDE server response (#6180)
* Mapped pre-TDE server response to UserDecryptionOptions. * Updated logic on SsoLoginStrategy to match account. * Linting. * Adjusted tests. * Fixed tests.
This commit is contained in:
parent
a920d62dfe
commit
182d5bf5ac
@ -41,10 +41,7 @@ import { IdentityCaptchaResponse } from "../models/response/identity-captcha.res
|
|||||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||||
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
|
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
|
||||||
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
|
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
|
||||||
import {
|
import { IUserDecryptionOptionsServerResponse } from "../models/response/user-decryption-options/user-decryption-options.response";
|
||||||
IUserDecryptionOptionsServerResponse,
|
|
||||||
UserDecryptionOptionsResponse,
|
|
||||||
} from "../models/response/user-decryption-options/user-decryption-options.response";
|
|
||||||
|
|
||||||
import { PasswordLogInStrategy } from "./password-login.strategy";
|
import { PasswordLogInStrategy } from "./password-login.strategy";
|
||||||
|
|
||||||
@ -65,10 +62,6 @@ const name = "NAME";
|
|||||||
const defaultUserDecryptionOptionsServerResponse: IUserDecryptionOptionsServerResponse = {
|
const defaultUserDecryptionOptionsServerResponse: IUserDecryptionOptionsServerResponse = {
|
||||||
HasMasterPassword: true,
|
HasMasterPassword: true,
|
||||||
};
|
};
|
||||||
const userDecryptionOptions = new UserDecryptionOptionsResponse(
|
|
||||||
defaultUserDecryptionOptionsServerResponse
|
|
||||||
);
|
|
||||||
const acctDecryptionOptions = AccountDecryptionOptions.fromResponse(userDecryptionOptions);
|
|
||||||
|
|
||||||
const decodedToken = {
|
const decodedToken = {
|
||||||
sub: userId,
|
sub: userId,
|
||||||
@ -197,7 +190,7 @@ describe("LogInStrategy", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
keys: new AccountKeys(),
|
keys: new AccountKeys(),
|
||||||
decryptionOptions: acctDecryptionOptions,
|
decryptionOptions: AccountDecryptionOptions.fromResponse(idTokenResponse),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
|
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
|
||||||
|
@ -143,9 +143,7 @@ export abstract class LogInStrategy {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
keys: accountKeys,
|
keys: accountKeys,
|
||||||
decryptionOptions: AccountDecryptionOptions.fromResponse(
|
decryptionOptions: AccountDecryptionOptions.fromResponse(tokenResponse),
|
||||||
tokenResponse.userDecryptionOptions
|
|
||||||
),
|
|
||||||
adminAuthRequest: adminAuthRequest?.toJSON(),
|
adminAuthRequest: adminAuthRequest?.toJSON(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -266,7 +266,61 @@ describe("SsoLogInStrategy", () => {
|
|||||||
describe("Key Connector", () => {
|
describe("Key Connector", () => {
|
||||||
let tokenResponse: IdentityTokenResponse;
|
let tokenResponse: IdentityTokenResponse;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tokenResponse = identityTokenResponseFactory(null, { HasMasterPassword: false });
|
tokenResponse = identityTokenResponseFactory(null, {
|
||||||
|
HasMasterPassword: false,
|
||||||
|
KeyConnectorOption: { KeyConnectorUrl: keyConnectorUrl },
|
||||||
|
});
|
||||||
|
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets and sets the master key if Key Connector is enabled and the user doesn't have a master password", async () => {
|
||||||
|
const masterKey = new SymmetricCryptoKey(
|
||||||
|
new Uint8Array(64).buffer as CsprngArray
|
||||||
|
) as MasterKey;
|
||||||
|
|
||||||
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
|
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||||
|
|
||||||
|
await ssoLogInStrategy.logIn(credentials);
|
||||||
|
|
||||||
|
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts new SSO user with no master password to Key Connector on first login", async () => {
|
||||||
|
tokenResponse.key = null;
|
||||||
|
|
||||||
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
|
|
||||||
|
await ssoLogInStrategy.logIn(credentials);
|
||||||
|
|
||||||
|
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
||||||
|
tokenResponse,
|
||||||
|
ssoOrgId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decrypts and sets the user key if Key Connector is enabled and the user doesn't have a master password", async () => {
|
||||||
|
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||||
|
const masterKey = new SymmetricCryptoKey(
|
||||||
|
new Uint8Array(64).buffer as CsprngArray
|
||||||
|
) as MasterKey;
|
||||||
|
|
||||||
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
|
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||||
|
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||||
|
|
||||||
|
await ssoLogInStrategy.logIn(credentials);
|
||||||
|
|
||||||
|
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
||||||
|
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Key Connector Pre-TDE", () => {
|
||||||
|
let tokenResponse: IdentityTokenResponse;
|
||||||
|
beforeEach(() => {
|
||||||
|
tokenResponse = identityTokenResponseFactory();
|
||||||
|
tokenResponse.userDecryptionOptions = null;
|
||||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -101,16 +101,22 @@ export class SsoLogInStrategy extends LogInStrategy {
|
|||||||
private shouldSetMasterKeyFromKeyConnector(tokenResponse: IdentityTokenResponse): boolean {
|
private shouldSetMasterKeyFromKeyConnector(tokenResponse: IdentityTokenResponse): boolean {
|
||||||
const userDecryptionOptions = tokenResponse?.userDecryptionOptions;
|
const userDecryptionOptions = tokenResponse?.userDecryptionOptions;
|
||||||
|
|
||||||
// If the user has a master password, this means that they need to migrate to Key Connector, so we won't set the key here.
|
if (userDecryptionOptions != null) {
|
||||||
// We default to false here because old server versions won't have hasMasterPassword and in that case we want to rely solely on the keyConnectorUrl.
|
const userHasMasterPassword = userDecryptionOptions.hasMasterPassword;
|
||||||
// TODO: remove null default after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
const userHasKeyConnectorUrl =
|
||||||
const userHasMasterPassword = userDecryptionOptions?.hasMasterPassword ?? false;
|
userDecryptionOptions.keyConnectorOption?.keyConnectorUrl != null;
|
||||||
|
|
||||||
const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse);
|
|
||||||
|
|
||||||
// In order for us to set the master key from Key Connector, we need to have a Key Connector URL
|
// In order for us to set the master key from Key Connector, we need to have a Key Connector URL
|
||||||
// and the user must not have a master password.
|
// and the user must not have a master password.
|
||||||
return keyConnectorUrl != null && !userHasMasterPassword;
|
return userHasKeyConnectorUrl && !userHasMasterPassword;
|
||||||
|
} else {
|
||||||
|
// In pre-TDE versions of the server, the userDecryptionOptions will not be present.
|
||||||
|
// In this case, we can determine if the user has a master password and has a Key Connector URL by
|
||||||
|
// just checking the keyConnectorUrl property. This is because the server short-circuits on the response
|
||||||
|
// and will not pass back the URL in the response if the user has a master password.
|
||||||
|
// TODO: remove compatibility check after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
||||||
|
return tokenResponse.keyConnectorUrl != null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getKeyConnectorUrl(tokenResponse: IdentityTokenResponse): string {
|
private getKeyConnectorUrl(tokenResponse: IdentityTokenResponse): string {
|
||||||
|
@ -11,7 +11,7 @@ import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
|
|||||||
import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason";
|
import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason";
|
||||||
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
|
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
|
||||||
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||||
import { UserDecryptionOptionsResponse } from "../../../auth/models/response/user-decryption-options/user-decryption-options.response";
|
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
|
||||||
import { KdfType, UriMatchType } from "../../../enums";
|
import { KdfType, UriMatchType } from "../../../enums";
|
||||||
import { EventData } from "../../../models/data/event.data";
|
import { EventData } from "../../../models/data/event.data";
|
||||||
import { GeneratedPasswordHistory } from "../../../tools/generator/password";
|
import { GeneratedPasswordHistory } from "../../../tools/generator/password";
|
||||||
@ -311,28 +311,46 @@ export class AccountDecryptionOptions {
|
|||||||
// return this.keyConnectorOption !== null && this.keyConnectorOption !== undefined;
|
// return this.keyConnectorOption !== null && this.keyConnectorOption !== undefined;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
static fromResponse(response: UserDecryptionOptionsResponse): AccountDecryptionOptions {
|
static fromResponse(response: IdentityTokenResponse): AccountDecryptionOptions {
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountDecryptionOptions = new AccountDecryptionOptions();
|
const accountDecryptionOptions = new AccountDecryptionOptions();
|
||||||
accountDecryptionOptions.hasMasterPassword = response.hasMasterPassword;
|
|
||||||
|
|
||||||
if (response.trustedDeviceOption) {
|
if (response.userDecryptionOptions) {
|
||||||
|
// If the response has userDecryptionOptions, this means it's on a post-TDE server version and can interrogate
|
||||||
|
// the new decryption options.
|
||||||
|
const responseOptions = response.userDecryptionOptions;
|
||||||
|
accountDecryptionOptions.hasMasterPassword = responseOptions.hasMasterPassword;
|
||||||
|
|
||||||
|
if (responseOptions.trustedDeviceOption) {
|
||||||
accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption(
|
accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption(
|
||||||
response.trustedDeviceOption.hasAdminApproval,
|
responseOptions.trustedDeviceOption.hasAdminApproval,
|
||||||
response.trustedDeviceOption.hasLoginApprovingDevice,
|
responseOptions.trustedDeviceOption.hasLoginApprovingDevice,
|
||||||
response.trustedDeviceOption.hasManageResetPasswordPermission
|
responseOptions.trustedDeviceOption.hasManageResetPasswordPermission
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.keyConnectorOption) {
|
if (responseOptions.keyConnectorOption) {
|
||||||
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
|
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
|
||||||
response.keyConnectorOption.keyConnectorUrl
|
responseOptions.keyConnectorOption.keyConnectorUrl
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// If the response does not have userDecryptionOptions, this means it's on a pre-TDE server version and so
|
||||||
|
// we must base our decryption options on the presence of the keyConnectorUrl.
|
||||||
|
// Note that the presence of keyConnectorUrl implies that the user does not have a master password, as in pre-TDE
|
||||||
|
// server versions, a master password short-circuited the addition of the keyConnectorUrl to the response.
|
||||||
|
// TODO: remove this check after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
||||||
|
const usingKeyConnector = response.keyConnectorUrl != null;
|
||||||
|
accountDecryptionOptions.hasMasterPassword = !usingKeyConnector;
|
||||||
|
if (usingKeyConnector) {
|
||||||
|
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
|
||||||
|
response.keyConnectorUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return accountDecryptionOptions;
|
return accountDecryptionOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user