1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-21 10:43:35 +00:00

Merge remote-tracking branch 'origin/master' into feature/flexible-collections

This commit is contained in:
Thomas Rittson
2023-09-26 10:44:01 +10:00
284 changed files with 3025 additions and 1935 deletions

View File

@@ -141,6 +141,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
await this.loginService.saveEmailSettings();
if (this.handleCaptchaRequired(response)) {
return;
} else if (this.handleMigrateEncryptionKey(response)) {
return;
} else if (response.requiresTwoFactor) {
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
this.onSuccessfulLoginTwoFactorNavigate();
@@ -272,6 +274,21 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
await this.loginService.saveEmailSettings();
}
// Legacy accounts used the master key to encrypt data. Migration is required
// but only performed on web
protected handleMigrateEncryptionKey(result: AuthResult): boolean {
if (!result.requiresEncryptionKeyMigration) {
return false;
}
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccured"),
this.i18nService.t("encryptionKeyMigrationRequired")
);
return true;
}
private getErrorToastMessage() {
const error: AllValidationErrors = this.formValidationErrorService
.getFormValidationErrors(this.formGroup.controls)

View File

@@ -215,9 +215,24 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
await this.handleLoginResponse(authResult);
}
protected handleMigrateEncryptionKey(result: AuthResult): boolean {
if (!result.requiresEncryptionKeyMigration) {
return false;
}
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccured"),
this.i18nService.t("encryptionKeyMigrationRequired")
);
return true;
}
private async handleLoginResponse(authResult: AuthResult) {
if (this.handleCaptchaRequired(authResult)) {
return;
} else if (this.handleMigrateEncryptionKey(authResult)) {
return;
}
this.loginService.clearValues();

View File

@@ -10,7 +10,10 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ClientType } from "@bitwarden/common/enums";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
/**
* Only allow access to this route if the vault is locked.
@@ -25,9 +28,21 @@ export function lockGuard(): CanActivateFn {
const authService = inject(AuthService);
const cryptoService = inject(CryptoService);
const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction);
const platformUtilService = inject(PlatformUtilsService);
const messagingService = inject(MessagingService);
const router = inject(Router);
const userVerificationService = inject(UserVerificationService);
// If legacy user on web, redirect to migration page
if (await cryptoService.isLegacyUser()) {
if (platformUtilService.getClientType() === ClientType.Web) {
return router.createUrlTree(["migrate-legacy-encryption"]);
}
// Log out legacy users on other clients
messagingService.send("logout");
return false;
}
const authStatus = await authService.getAuthStatus();
if (authStatus !== AuthenticationStatus.Locked) {
return router.createUrlTree(["/"]);

View File

@@ -272,6 +272,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
this.previousCipherId = this.cipherId;
this.reprompt = this.cipher.reprompt !== CipherRepromptType.None;
if (this.reprompt) {
this.cipher.login.autofillOnPageLoad = this.autofillOnPageLoadOptions[2].value;
}
}
async submit(): Promise<boolean> {
@@ -570,8 +573,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.reprompt = !this.reprompt;
if (this.reprompt) {
this.cipher.reprompt = CipherRepromptType.Password;
this.cipher.login.autofillOnPageLoad = this.autofillOnPageLoadOptions[2].value;
} else {
this.cipher.reprompt = CipherRepromptType.None;
this.cipher.login.autofillOnPageLoad = this.autofillOnPageLoadOptions[0].value;
}
}

View File

@@ -24,7 +24,6 @@ export class AttachmentsComponent implements OnInit {
cipher: CipherView;
cipherDomain: Cipher;
hasUpdatedKey: boolean;
canAccessAttachments: boolean;
formPromise: Promise<any>;
deletePromises: { [id: string]: Promise<any> } = {};
@@ -50,15 +49,6 @@ export class AttachmentsComponent implements OnInit {
}
async submit() {
if (!this.hasUpdatedKey) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("updateKey")
);
return;
}
const fileEl = document.getElementById("file") as HTMLInputElement;
const files = fileEl.files;
if (files == null || files.length === 0) {
@@ -191,7 +181,6 @@ export class AttachmentsComponent implements OnInit {
this.cipherDomain = await this.loadCipher();
this.cipher = await this.cipherDomain.decrypt();
this.hasUpdatedKey = await this.cryptoService.hasUserKey();
const canAccessPremium = await this.stateService.getCanAccessPremium();
this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null;
@@ -206,19 +195,6 @@ export class AttachmentsComponent implements OnInit {
if (confirmed) {
this.platformUtilsService.launchUri("https://vault.bitwarden.com/#/?premium=purchase");
}
} else if (!this.hasUpdatedKey) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "featureUnavailable" },
content: { key: "updateKey" },
acceptButtonText: { key: "learnMore" },
type: "warning",
});
if (confirmed) {
this.platformUtilsService.launchUri(
"https://bitwarden.com/help/account-encryption-key/#rotate-your-encryption-key"
);
}
}
}

View File

@@ -1,4 +1,5 @@
import { ApiService } from "../../abstractions/api.service";
import { ClientType } from "../../enums";
import { KeysRequest } from "../../models/request/keys.request";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
@@ -151,6 +152,16 @@ export abstract class LogInStrategy {
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
const result = new AuthResult();
// Old encryption keys must be migrated, but is currently only available on web.
// Other clients shouldn't continue the login process.
if (this.encryptionKeyMigrationRequired(response)) {
result.requiresEncryptionKeyMigration = true;
if (this.platformUtilsService.getClientType() !== ClientType.Web) {
return result;
}
}
result.resetMasterPassword = response.resetMasterPassword;
// Convert boolean to enum
@@ -166,9 +177,7 @@ export abstract class LogInStrategy {
}
await this.setMasterKey(response);
await this.setUserKey(response);
await this.setPrivateKey(response);
this.messagingService.send("loggedIn");
@@ -183,6 +192,12 @@ export abstract class LogInStrategy {
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
// Old accounts used master key for encryption. We are forcing migrations but only need to
// check on password logins
protected encryptionKeyMigrationRequired(response: IdentityTokenResponse): boolean {
return false;
}
protected async createKeyPairForOldAccount() {
try {
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();

View File

@@ -147,6 +147,10 @@ export class PasswordLogInStrategy extends LogInStrategy {
}
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
// If migration is required, we won't have a user key to set yet.
if (this.encryptionKeyMigrationRequired(response)) {
return;
}
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
const masterKey = await this.cryptoService.getMasterKey();
@@ -162,6 +166,10 @@ export class PasswordLogInStrategy extends LogInStrategy {
);
}
protected override encryptionKeyMigrationRequired(response: IdentityTokenResponse): boolean {
return !response.key;
}
private getMasterPasswordPolicyOptionsFromResponse(
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse
): MasterPasswordPolicyOptions {

View File

@@ -17,6 +17,7 @@ export class AuthResult {
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
ssoEmail2FaSessionToken?: string;
email: string;
requiresEncryptionKeyMigration: boolean;
get requiresCaptcha() {
return !Utils.isNullOrWhitespace(this.captchaSiteKey);

View File

@@ -3,7 +3,6 @@ export enum FeatureFlag {
DisplayLowKdfIterationWarningFlag = "display-kdf-iteration-warning",
TrustedDeviceEncryption = "trusted-device-encryption",
AutofillV2 = "autofill-v2",
SecretsManagerBilling = "sm-ga-billing",
}
// Replace this with a type safe lookup of the feature flag values in PM-2282

View File

@@ -42,6 +42,12 @@ export abstract class CryptoService {
* @returns The user key
*/
getUserKey: (userId?: string) => Promise<UserKey>;
/**
* Checks if the user is using an old encryption scheme that used the master key
* for encryption of data instead of the user key.
*/
isLegacyUser: (masterKey?: MasterKey, userId?: string) => Promise<boolean>;
/**
* Use for encryption/decryption of data in order to support legacy
* encryption models. It will return the user key if available,

View File

@@ -77,6 +77,12 @@ export class CryptoService implements CryptoServiceAbstraction {
}
}
async isLegacyUser(masterKey?: MasterKey, userId?: string): Promise<boolean> {
return await this.validateUserKey(
(masterKey ?? (await this.getMasterKey(userId))) as unknown as UserKey
);
}
async getUserKeyWithLegacySupport(userId?: string): Promise<UserKey> {
const userKey = await this.getUserKey(userId);
if (userKey) {
@@ -510,7 +516,8 @@ export class CryptoService implements CryptoServiceAbstraction {
}
async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]> {
key ||= await this.getUserKey();
// Default to user key
key ||= await this.getUserKeyWithLegacySupport();
const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
const publicB64 = Utils.fromBufferToB64(keyPair[0]);
@@ -943,23 +950,30 @@ export class CryptoService implements CryptoServiceAbstraction {
async migrateAutoKeyIfNeeded(userId?: string) {
const oldAutoKey = await this.stateService.getCryptoMasterKeyAuto({ userId: userId });
if (oldAutoKey) {
// decrypt
const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldAutoKey)) as MasterKey;
const encryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({
userId: userId,
});
const userKey = await this.decryptUserKeyWithMasterKey(
masterKey,
new EncString(encryptedUserKey),
userId
);
// migrate
await this.stateService.setUserKeyAutoUnlock(userKey.keyB64, { userId: userId });
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
// set encrypted user key in case user immediately locks without syncing
await this.setMasterKeyEncryptedUserKey(encryptedUserKey);
if (!oldAutoKey) {
return;
}
// Decrypt
const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldAutoKey)) as MasterKey;
if (await this.isLegacyUser(masterKey, userId)) {
// Legacy users don't have a user key, so no need to migrate.
// Instead, set the master key for additional isLegacyUser checks that will log the user out.
await this.setMasterKey(masterKey, userId);
return;
}
const encryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({
userId: userId,
});
const userKey = await this.decryptUserKeyWithMasterKey(
masterKey,
new EncString(encryptedUserKey),
userId
);
// Migrate
await this.stateService.setUserKeyAutoUnlock(userKey.keyB64, { userId: userId });
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
// Set encrypted user key in case user immediately locks without syncing
await this.setMasterKeyEncryptedUserKey(encryptedUserKey);
}
async decryptAndMigrateOldPinKey(

View File

@@ -5,6 +5,7 @@ import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/va
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout.service";
import { AuthService } from "../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { ClientType } from "../../enums";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
@@ -141,10 +142,18 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
}
private async migrateKeyForNeverLockIfNeeded(): Promise<void> {
// Web can't set vault timeout to never
if (this.platformUtilsService.getClientType() == ClientType.Web) {
return;
}
const accounts = await firstValueFrom(this.stateService.accounts$);
for (const userId in accounts) {
if (userId != null) {
await this.cryptoService.migrateAutoKeyIfNeeded(userId);
// Legacy users should be logged out since we're not on the web vault and can't migrate.
if (await this.cryptoService.isLegacyUser(null, userId)) {
await this.logOut(userId);
}
}
}
}

View File

@@ -329,11 +329,6 @@ export class CipherService implements CipherServiceAbstraction {
return await this.getDecryptedCipherCache();
}
const hasKey = await this.cryptoService.hasUserKey();
if (!hasKey) {
throw new Error("No user key found.");
}
const ciphers = await this.getAll();
const orgKeys = await this.cryptoService.getOrgKeys();
const userKey = await this.cryptoService.getUserKeyWithLegacySupport();
@@ -403,14 +398,21 @@ export class CipherService implements CipherServiceAbstraction {
defaultMatch ??= await this.stateService.getDefaultUriMatch();
return ciphers.filter((cipher) => {
if (cipher.deletedDate != null) {
const cipherIsLogin = cipher.type === CipherType.Login && cipher.login !== null;
if (cipher.deletedDate !== null) {
return false;
}
if (includeOtherTypes != null && includeOtherTypes.indexOf(cipher.type) > -1) {
if (
Array.isArray(includeOtherTypes) &&
includeOtherTypes.includes(cipher.type) &&
!cipherIsLogin
) {
return true;
}
if (cipher.type === CipherType.Login && cipher.login !== null) {
if (cipherIsLogin) {
return cipher.login.matchesUri(url, equivalentDomains, defaultMatch);
}

View File

@@ -157,6 +157,24 @@ export const Disabled: Story = {
args: {},
};
export const Readonly: Story = {
render: (args) => ({
props: args,
template: `
<bit-form-field>
<bit-label>Input</bit-label>
<input bitInput value="Foobar" readonly />
</bit-form-field>
<bit-form-field>
<bit-label>Textarea</bit-label>
<textarea bitInput rows="4" readonly>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</textarea>
</bit-form-field>
`,
}),
args: {},
};
export const InputGroup: Story = {
render: (args) => ({
props: args,

View File

@@ -44,6 +44,7 @@ export class BitInputDirective implements BitFormFieldControl {
"focus:tw-ring-primary-700",
"focus:tw-z-10",
"disabled:tw-bg-secondary-100",
"[&:is(input,textarea):read-only]:tw-bg-secondary-100",
].filter((s) => s != "");
}