1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

Merge branch 'main' into autofill/pm-5189-fix-issues-present-with-inline-menu-rendering-in-iframes

This commit is contained in:
Cesar Gonzalez
2024-04-16 14:03:51 -05:00
committed by GitHub
13 changed files with 144 additions and 82 deletions

View File

@@ -172,6 +172,12 @@
"changeMasterPassword": { "changeMasterPassword": {
"message": "Change master password" "message": "Change master password"
}, },
"continueToWebApp": {
"message": "Continue to web app?"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "You can change your master password on the Bitwarden web app."
},
"fingerprintPhrase": { "fingerprintPhrase": {
"message": "Fingerprint phrase", "message": "Fingerprint phrase",
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
@@ -557,12 +563,6 @@
"addedFolder": { "addedFolder": {
"message": "Folder added" "message": "Folder added"
}, },
"changeMasterPass": {
"message": "Change master password"
},
"changeMasterPasswordConfirmation": {
"message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?"
},
"twoStepLoginConfirmation": { "twoStepLoginConfirmation": {
"message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?"
}, },

View File

@@ -153,7 +153,7 @@
*ngIf="showChangeMasterPass" *ngIf="showChangeMasterPass"
> >
<div class="row-main">{{ "changeMasterPassword" | i18n }}</div> <div class="row-main">{{ "changeMasterPassword" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i> <i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
</button> </button>
<button <button
type="button" type="button"

View File

@@ -441,9 +441,10 @@ export class SettingsComponent implements OnInit {
async changePassword() { async changePassword() {
const confirmed = await this.dialogService.openSimpleDialog({ const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "changeMasterPassword" }, title: { key: "continueToWebApp" },
content: { key: "changeMasterPasswordConfirmation" }, content: { key: "changeMasterPasswordOnWebConfirmation" },
type: "info", type: "info",
acceptButtonText: { key: "continue" },
}); });
if (confirmed) { if (confirmed) {
const env = await firstValueFrom(this.environmentService.environment$); const env = await firstValueFrom(this.environmentService.environment$);

View File

@@ -800,8 +800,11 @@
"changeMasterPass": { "changeMasterPass": {
"message": "Change master password" "message": "Change master password"
}, },
"changeMasterPasswordConfirmation": { "continueToWebApp": {
"message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" "message": "Continue to web app?"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "You can change your master password on the Bitwarden web app."
}, },
"fingerprintPhrase": { "fingerprintPhrase": {
"message": "Fingerprint phrase", "message": "Fingerprint phrase",

View File

@@ -65,10 +65,10 @@ export class AccountMenu implements IMenubarMenu {
id: "changeMasterPass", id: "changeMasterPass",
click: async () => { click: async () => {
const result = await dialog.showMessageBox(this._window, { const result = await dialog.showMessageBox(this._window, {
title: this.localize("changeMasterPass"), title: this.localize("continueToWebApp"),
message: this.localize("changeMasterPass"), message: this.localize("continueToWebApp"),
detail: this.localize("changeMasterPasswordConfirmation"), detail: this.localize("changeMasterPasswordOnWebConfirmation"),
buttons: [this.localize("yes"), this.localize("no")], buttons: [this.localize("continue"), this.localize("cancel")],
cancelId: 1, cancelId: 1,
defaultId: 0, defaultId: 0,
noLink: true, noLink: true,

View File

@@ -166,8 +166,8 @@ export abstract class LoginStrategy {
const userId = accountInformation.sub; const userId = accountInformation.sub;
const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId });
const vaultTimeout = await this.stateService.getVaultTimeout(); const vaultTimeout = await this.stateService.getVaultTimeout({ userId });
// set access token and refresh token before account initialization so authN status can be accurate // set access token and refresh token before account initialization so authN status can be accurate
// User id will be derived from the access token. // User id will be derived from the access token.

View File

@@ -23,7 +23,6 @@ import {
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
REFRESH_TOKEN_DISK, REFRESH_TOKEN_DISK,
REFRESH_TOKEN_MEMORY, REFRESH_TOKEN_MEMORY,
REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
} from "./token.state"; } from "./token.state";
describe("TokenService", () => { describe("TokenService", () => {
@@ -1120,20 +1119,13 @@ describe("TokenService", () => {
secureStorageOptions, secureStorageOptions,
); );
// assert data was migrated out of disk and memory + flag was set // assert data was migrated out of disk and memory
expect( expect(
singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK).nextMock, singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK).nextMock,
).toHaveBeenCalledWith(null); ).toHaveBeenCalledWith(null);
expect( expect(
singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY).nextMock, singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY).nextMock,
).toHaveBeenCalledWith(null); ).toHaveBeenCalledWith(null);
expect(
singleUserStateProvider.getFake(
userIdFromAccessToken,
REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
).nextMock,
).toHaveBeenCalledWith(true);
}); });
}); });
}); });
@@ -1260,11 +1252,6 @@ describe("TokenService", () => {
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken); .stateSubject.next(userIdFromAccessToken);
// set access token migration flag to true
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE)
.stateSubject.next([userIdFromAccessToken, true]);
// Act // Act
const result = await tokenService.getRefreshToken(); const result = await tokenService.getRefreshToken();
// Assert // Assert
@@ -1284,11 +1271,6 @@ describe("TokenService", () => {
secureStorageService.get.mockResolvedValue(refreshToken); secureStorageService.get.mockResolvedValue(refreshToken);
// set access token migration flag to true
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE)
.stateSubject.next([userIdFromAccessToken, true]);
// Act // Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken); const result = await tokenService.getRefreshToken(userIdFromAccessToken);
// Assert // Assert
@@ -1305,11 +1287,6 @@ describe("TokenService", () => {
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]); .stateSubject.next([userIdFromAccessToken, refreshToken]);
// set refresh token migration flag to false
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE)
.stateSubject.next([userIdFromAccessToken, false]);
// Act // Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken); const result = await tokenService.getRefreshToken(userIdFromAccessToken);
@@ -1335,11 +1312,6 @@ describe("TokenService", () => {
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken); .stateSubject.next(userIdFromAccessToken);
// set access token migration flag to false
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE)
.stateSubject.next([userIdFromAccessToken, false]);
// Act // Act
const result = await tokenService.getRefreshToken(); const result = await tokenService.getRefreshToken();

View File

@@ -32,7 +32,6 @@ import {
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
REFRESH_TOKEN_DISK, REFRESH_TOKEN_DISK,
REFRESH_TOKEN_MEMORY, REFRESH_TOKEN_MEMORY,
REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
} from "./token.state"; } from "./token.state";
export enum TokenStorageLocation { export enum TokenStorageLocation {
@@ -441,9 +440,6 @@ export class TokenService implements TokenServiceAbstraction {
await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null); await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null);
await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MEMORY).update((_) => null); await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MEMORY).update((_) => null);
// Set flag to indicate that the refresh token has been migrated to secure storage (don't remove this)
await this.setRefreshTokenMigratedToSecureStorage(userId);
return; return;
case TokenStorageLocation.Disk: case TokenStorageLocation.Disk:
@@ -467,12 +463,6 @@ export class TokenService implements TokenServiceAbstraction {
return undefined; return undefined;
} }
const refreshTokenMigratedToSecureStorage =
await this.getRefreshTokenMigratedToSecureStorage(userId);
if (this.platformSupportsSecureStorage && refreshTokenMigratedToSecureStorage) {
return await this.getStringFromSecureStorage(userId, this.refreshTokenSecureStorageKey);
}
// pre-secure storage migration: // pre-secure storage migration:
// Always read memory first b/c faster // Always read memory first b/c faster
const refreshTokenMemory = await this.getStateValueByUserIdAndKeyDef( const refreshTokenMemory = await this.getStateValueByUserIdAndKeyDef(
@@ -484,13 +474,24 @@ export class TokenService implements TokenServiceAbstraction {
return refreshTokenMemory; return refreshTokenMemory;
} }
// if memory is null, read from disk // if memory is null, read from disk and then secure storage
const refreshTokenDisk = await this.getStateValueByUserIdAndKeyDef(userId, REFRESH_TOKEN_DISK); const refreshTokenDisk = await this.getStateValueByUserIdAndKeyDef(userId, REFRESH_TOKEN_DISK);
if (refreshTokenDisk != null) { if (refreshTokenDisk != null) {
return refreshTokenDisk; return refreshTokenDisk;
} }
if (this.platformSupportsSecureStorage) {
const refreshTokenSecureStorage = await this.getStringFromSecureStorage(
userId,
this.refreshTokenSecureStorageKey,
);
if (refreshTokenSecureStorage != null) {
return refreshTokenSecureStorage;
}
}
return null; return null;
} }
@@ -516,18 +517,6 @@ export class TokenService implements TokenServiceAbstraction {
await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null); await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null);
} }
private async getRefreshTokenMigratedToSecureStorage(userId: UserId): Promise<boolean> {
return await firstValueFrom(
this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE).state$,
);
}
private async setRefreshTokenMigratedToSecureStorage(userId: UserId): Promise<void> {
await this.singleUserStateProvider
.get(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE)
.update((_) => true);
}
async setClientId( async setClientId(
clientId: string, clientId: string,
vaultTimeoutAction: VaultTimeoutAction, vaultTimeoutAction: VaultTimeoutAction,

View File

@@ -10,7 +10,6 @@ import {
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
REFRESH_TOKEN_DISK, REFRESH_TOKEN_DISK,
REFRESH_TOKEN_MEMORY, REFRESH_TOKEN_MEMORY,
REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
} from "./token.state"; } from "./token.state";
describe.each([ describe.each([
@@ -18,7 +17,6 @@ describe.each([
[ACCESS_TOKEN_MEMORY, "accessTokenMemory"], [ACCESS_TOKEN_MEMORY, "accessTokenMemory"],
[REFRESH_TOKEN_DISK, "refreshTokenDisk"], [REFRESH_TOKEN_DISK, "refreshTokenDisk"],
[REFRESH_TOKEN_MEMORY, "refreshTokenMemory"], [REFRESH_TOKEN_MEMORY, "refreshTokenMemory"],
[REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, true],
[EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, { user: "token" }], [EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, { user: "token" }],
[API_KEY_CLIENT_ID_DISK, "apiKeyClientIdDisk"], [API_KEY_CLIENT_ID_DISK, "apiKeyClientIdDisk"],
[API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"], [API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"],

View File

@@ -30,15 +30,6 @@ export const REFRESH_TOKEN_MEMORY = new UserKeyDefinition<string>(TOKEN_MEMORY,
clearOn: [], // Manually handled clearOn: [], // Manually handled
}); });
export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE = new UserKeyDefinition<boolean>(
TOKEN_DISK,
"refreshTokenMigratedToSecureStorage",
{
deserializer: (refreshTokenMigratedToSecureStorage) => refreshTokenMigratedToSecureStorage,
clearOn: [], // Don't clear on lock/logout so that we always check the correct place (secure storage) for the refresh token if it's been migrated
},
);
export const EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL = KeyDefinition.record<string, string>( export const EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL = KeyDefinition.record<string, string>(
TOKEN_DISK_LOCAL, TOKEN_DISK_LOCAL,
"emailTwoFactorTokenRecord", "emailTwoFactorTokenRecord",

View File

@@ -54,6 +54,7 @@ import { SendMigrator } from "./migrations/54-move-encrypted-sends";
import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider"; import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider";
import { AuthRequestMigrator } from "./migrations/56-move-auth-requests"; import { AuthRequestMigrator } from "./migrations/56-move-auth-requests";
import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider"; import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider";
import { RemoveRefreshTokenMigratedFlagMigrator } from "./migrations/58-remove-refresh-token-migrated-state-provider-flag";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
@@ -61,7 +62,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version"; import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3; export const MIN_VERSION = 3;
export const CURRENT_VERSION = 57; export const CURRENT_VERSION = 58;
export type MinVersion = typeof MIN_VERSION; export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() { export function createMigrationBuilder() {
@@ -120,7 +121,8 @@ export function createMigrationBuilder() {
.with(SendMigrator, 53, 54) .with(SendMigrator, 53, 54)
.with(MoveMasterKeyStateToProviderMigrator, 54, 55) .with(MoveMasterKeyStateToProviderMigrator, 54, 55)
.with(AuthRequestMigrator, 55, 56) .with(AuthRequestMigrator, 55, 56)
.with(CipherServiceMigrator, 56, CURRENT_VERSION); .with(CipherServiceMigrator, 56, 57)
.with(RemoveRefreshTokenMigratedFlagMigrator, 57, CURRENT_VERSION);
} }
export async function currentVersion( export async function currentVersion(

View File

@@ -0,0 +1,72 @@
import { MockProxy, any } from "jest-mock-extended";
import { MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import { IRREVERSIBLE } from "../migrator";
import {
REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
RemoveRefreshTokenMigratedFlagMigrator,
} from "./58-remove-refresh-token-migrated-state-provider-flag";
// Represents data in state service pre-migration
function preMigrationJson() {
return {
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user1", "user2", "user3"],
user_user1_token_refreshTokenMigratedToSecureStorage: true,
user_user2_token_refreshTokenMigratedToSecureStorage: false,
};
}
function rollbackJSON() {
return {
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user1", "user2", "user3"],
};
}
describe("RemoveRefreshTokenMigratedFlagMigrator", () => {
let helper: MockProxy<MigrationHelper>;
let sut: RemoveRefreshTokenMigratedFlagMigrator;
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(preMigrationJson(), 57);
sut = new RemoveRefreshTokenMigratedFlagMigrator(57, 58);
});
it("should remove refreshTokenMigratedToSecureStorage from state provider for all accounts that have it", async () => {
await sut.migrate(helper);
expect(helper.removeFromUser).toHaveBeenCalledWith(
"user1",
REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
);
expect(helper.removeFromUser).toHaveBeenCalledWith(
"user2",
REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
);
expect(helper.removeFromUser).toHaveBeenCalledTimes(2);
expect(helper.removeFromUser).not.toHaveBeenCalledWith("user3", any());
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJSON(), 58);
sut = new RemoveRefreshTokenMigratedFlagMigrator(57, 58);
});
it("should not add data back and throw IRREVERSIBLE error on call", async () => {
await expect(sut.rollback(helper)).rejects.toThrow(IRREVERSIBLE);
});
});
});

View File

@@ -0,0 +1,34 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { IRREVERSIBLE, Migrator } from "../migrator";
type ExpectedAccountType = NonNullable<unknown>;
export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE: KeyDefinitionLike = {
key: "refreshTokenMigratedToSecureStorage", // matches KeyDefinition.key in DeviceTrustCryptoService
stateDefinition: {
name: "token", // matches StateDefinition.name in StateDefinitions
},
};
export class RemoveRefreshTokenMigratedFlagMigrator extends Migrator<57, 58> {
async migrate(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
const refreshTokenMigratedFlag = await helper.getFromUser(
userId,
REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
);
if (refreshTokenMigratedFlag != null) {
// Only delete the flag if it exists
await helper.removeFromUser(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE);
}
}
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
}
async rollback(helper: MigrationHelper): Promise<void> {
throw IRREVERSIBLE;
}
}