mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01:00
[PM-3533] Support onboarding Key Connector users with existing master passwords (#6082)
* Added checks for new KeyConnector URL in all references to the legacy one. * Updated KeyConnector logoutCallback to be a Promise * Removed extra dependencies from KeyConnectorService * Made the logout callback async. * Adjusted logic to handle having a master password. * Updated not to return error if master key is not found. * Undid change to callback to reduce scope of this change. * Cleaned up functions. * Updated tests. * Updated comments. * Updated comments. * Updated to use getKeyConnectorUrl helper.
This commit is contained in:
parent
26c330ff60
commit
e215828e85
@ -572,8 +572,6 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
|||||||
LogService,
|
LogService,
|
||||||
OrganizationServiceAbstraction,
|
OrganizationServiceAbstraction,
|
||||||
CryptoFunctionServiceAbstraction,
|
CryptoFunctionServiceAbstraction,
|
||||||
SyncNotifierServiceAbstraction,
|
|
||||||
MessagingServiceAbstraction,
|
|
||||||
LOGOUT_CALLBACK,
|
LOGOUT_CALLBACK,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -266,11 +266,11 @@ describe("SsoLogInStrategy", () => {
|
|||||||
describe("Key Connector", () => {
|
describe("Key Connector", () => {
|
||||||
let tokenResponse: IdentityTokenResponse;
|
let tokenResponse: IdentityTokenResponse;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tokenResponse = identityTokenResponseFactory();
|
tokenResponse = identityTokenResponseFactory(null, { HasMasterPassword: false });
|
||||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gets and sets the master key if Key Connector is enabled", async () => {
|
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(
|
const masterKey = new SymmetricCryptoKey(
|
||||||
new Uint8Array(64).buffer as CsprngArray
|
new Uint8Array(64).buffer as CsprngArray
|
||||||
) as MasterKey;
|
) as MasterKey;
|
||||||
@ -283,7 +283,7 @@ describe("SsoLogInStrategy", () => {
|
|||||||
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
|
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("converts new SSO user to Key Connector on first login", async () => {
|
it("converts new SSO user with no master password to Key Connector on first login", async () => {
|
||||||
tokenResponse.key = null;
|
tokenResponse.key = null;
|
||||||
|
|
||||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
@ -296,7 +296,7 @@ describe("SsoLogInStrategy", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("decrypts and sets the user key if Key Connector is enabled", async () => {
|
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 userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||||
const masterKey = new SymmetricCryptoKey(
|
const masterKey = new SymmetricCryptoKey(
|
||||||
new Uint8Array(64).buffer as CsprngArray
|
new Uint8Array(64).buffer as CsprngArray
|
||||||
|
@ -77,19 +77,50 @@ export class SsoLogInStrategy extends LogInStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override async setMasterKey(tokenResponse: IdentityTokenResponse) {
|
protected override async setMasterKey(tokenResponse: IdentityTokenResponse) {
|
||||||
// TODO: discuss how this is no longer true with TDE
|
// The only way we can be setting a master key at this point is if we are using Key Connector.
|
||||||
// eventually we’ll need to support migration of existing TDE users to Key Connector
|
// First, check to make sure that we should do so based on the token response.
|
||||||
|
if (this.shouldSetMasterKeyFromKeyConnector(tokenResponse)) {
|
||||||
|
// If we're here, we know that the user should use Key Connector (they have a KeyConnectorUrl) and does not have a master password.
|
||||||
|
// We can now check the key on the token response to see whether they are a brand new user or an existing user.
|
||||||
|
// The presence of a masterKeyEncryptedUserKey indicates that the user has already been provisioned in Key Connector.
|
||||||
const newSsoUser = tokenResponse.key == null;
|
const newSsoUser = tokenResponse.key == null;
|
||||||
|
if (newSsoUser) {
|
||||||
if (tokenResponse.keyConnectorUrl != null) {
|
|
||||||
if (!newSsoUser) {
|
|
||||||
await this.keyConnectorService.setMasterKeyFromUrl(tokenResponse.keyConnectorUrl);
|
|
||||||
} else {
|
|
||||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
|
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
|
||||||
|
} else {
|
||||||
|
const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse);
|
||||||
|
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if it is possible set the `masterKey` from Key Connector.
|
||||||
|
* @param tokenResponse
|
||||||
|
* @returns `true` if the master key can be set from Key Connector, `false` otherwise
|
||||||
|
*/
|
||||||
|
private shouldSetMasterKeyFromKeyConnector(tokenResponse: IdentityTokenResponse): boolean {
|
||||||
|
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.
|
||||||
|
// 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.
|
||||||
|
// TODO: remove null default after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
||||||
|
const userHasMasterPassword = userDecryptionOptions?.hasMasterPassword ?? false;
|
||||||
|
|
||||||
|
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
|
||||||
|
// and the user must not have a master password.
|
||||||
|
return keyConnectorUrl != null && !userHasMasterPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getKeyConnectorUrl(tokenResponse: IdentityTokenResponse): string {
|
||||||
|
// TODO: remove tokenResponse.keyConnectorUrl reference after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
||||||
|
const userDecryptionOptions = tokenResponse?.userDecryptionOptions;
|
||||||
|
return (
|
||||||
|
tokenResponse.keyConnectorUrl ?? userDecryptionOptions?.keyConnectorOption?.keyConnectorUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: future passkey login strategy will need to support setting user key (decrypting via TDE or admin approval request)
|
// TODO: future passkey login strategy will need to support setting user key (decrypting via TDE or admin approval request)
|
||||||
// so might be worth moving this logic to a common place (base login strategy or a separate service?)
|
// so might be worth moving this logic to a common place (base login strategy or a separate service?)
|
||||||
protected override async setUserKey(tokenResponse: IdentityTokenResponse): Promise<void> {
|
protected override async setUserKey(tokenResponse: IdentityTokenResponse): Promise<void> {
|
||||||
@ -117,9 +148,8 @@ export class SsoLogInStrategy extends LogInStrategy {
|
|||||||
await this.trySetUserKeyWithDeviceKey(tokenResponse);
|
await this.trySetUserKeyWithDeviceKey(tokenResponse);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
// TODO: remove tokenResponse.keyConnectorUrl when it's deprecated
|
|
||||||
masterKeyEncryptedUserKey != null &&
|
masterKeyEncryptedUserKey != null &&
|
||||||
(tokenResponse.keyConnectorUrl || userDecryptionOptions?.keyConnectorOption?.keyConnectorUrl)
|
this.getKeyConnectorUrl(tokenResponse) != null
|
||||||
) {
|
) {
|
||||||
// Key connector enabled for user
|
// Key connector enabled for user
|
||||||
await this.trySetUserKeyWithMasterKey();
|
await this.trySetUserKeyWithMasterKey();
|
||||||
@ -208,12 +238,15 @@ export class SsoLogInStrategy extends LogInStrategy {
|
|||||||
private async trySetUserKeyWithMasterKey(): Promise<void> {
|
private async trySetUserKeyWithMasterKey(): Promise<void> {
|
||||||
const masterKey = await this.cryptoService.getMasterKey();
|
const masterKey = await this.cryptoService.getMasterKey();
|
||||||
|
|
||||||
|
// There is a scenario in which the master key is not set here. That will occur if the user
|
||||||
|
// has a master password and is using Key Connector. In that case, we cannot set the master key
|
||||||
|
// because the user hasn't entered their master password yet.
|
||||||
|
// Instead, we'll return here and let the migration to Key Connector handle setting the master key.
|
||||||
if (!masterKey) {
|
if (!masterKey) {
|
||||||
throw new Error("Master key not found");
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
|
|
||||||
await this.cryptoService.setUserKey(userKey);
|
await this.cryptoService.setUserKey(userKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private cryptoFunctionService: CryptoFunctionService,
|
private cryptoFunctionService: CryptoFunctionService,
|
||||||
private logoutCallback: (expired: boolean, userId?: string) => void
|
private logoutCallback: (expired: boolean, userId?: string) => Promise<void>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
setUsesKeyConnector(usesKeyConnector: boolean) {
|
setUsesKeyConnector(usesKeyConnector: boolean) {
|
||||||
@ -84,7 +84,15 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async convertNewSsoUserToKeyConnector(tokenResponse: IdentityTokenResponse, orgId: string) {
|
async convertNewSsoUserToKeyConnector(tokenResponse: IdentityTokenResponse, orgId: string) {
|
||||||
const { kdf, kdfIterations, kdfMemory, kdfParallelism, keyConnectorUrl } = tokenResponse;
|
// TODO: Remove after tokenResponse.keyConnectorUrl is deprecated in 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
||||||
|
const {
|
||||||
|
kdf,
|
||||||
|
kdfIterations,
|
||||||
|
kdfMemory,
|
||||||
|
kdfParallelism,
|
||||||
|
keyConnectorUrl: legacyKeyConnectorUrl,
|
||||||
|
userDecryptionOptions,
|
||||||
|
} = tokenResponse;
|
||||||
const password = await this.cryptoFunctionService.randomBytes(64);
|
const password = await this.cryptoFunctionService.randomBytes(64);
|
||||||
const kdfConfig = new KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
|
const kdfConfig = new KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
|
||||||
|
|
||||||
@ -104,6 +112,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||||||
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
|
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const keyConnectorUrl =
|
||||||
|
legacyKeyConnectorUrl ?? userDecryptionOptions?.keyConnectorOption?.keyConnectorUrl;
|
||||||
await this.apiService.postUserKeyToKeyConnector(keyConnectorUrl, keyConnectorRequest);
|
await this.apiService.postUserKeyToKeyConnector(keyConnectorUrl, keyConnectorRequest);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.handleKeyConnectorError(e);
|
this.handleKeyConnectorError(e);
|
||||||
|
Loading…
Reference in New Issue
Block a user