1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-27 04:03:00 +02:00

[PM-3726] Force migration of legacy user's encryption key (#6195)

* [PM-3726] migrate legacy user's encryption key

* [PM-3726] add 2fa support and pr feedback

* [PM-3726] revert launch.json & webpack.config changes

* [PM-3726] remove update key component
- also remove card in vault since legacy users can't login

* [PM-3726] Fix i18n & PR feedback

* [PM-3726] make standalone component

* [PM-3726] linter

* [PM-3726] missing await

* [PM-3726] logout legacy users with vault timeout to never

* [PM-3726] add await

* [PM-3726] skip auto key migration for legacy users

* [PM-3726] pr feedback

* [PM-3726] move check for web into migrate method

---------

Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
Jake Fink 2023-09-20 15:57:01 -04:00 committed by GitHub
parent 020018085a
commit 8c06508435
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 834 additions and 273 deletions

View File

@ -771,8 +771,8 @@
"featureUnavailable": {
"message": "Feature unavailable"
},
"updateKey": {
"message": "You cannot use this feature until you update your encryption key."
"encryptionKeyMigrationRequired": {
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
},
"premiumMembership": {
"message": "Premium membership"

View File

@ -209,6 +209,11 @@ export class LoginCommand {
new PasswordLogInCredentials(email, password, null, twoFactor)
);
}
if (response.requiresEncryptionKeyMigration) {
return Response.error(
"Encryption key migration required. Please login through the web vault to update your encryption key."
);
}
if (response.captchaSiteKey) {
const credentials = new PasswordLogInCredentials(email, password);
const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials);

View File

@ -475,8 +475,8 @@
"maxFileSize": {
"message": "Maximum file size is 500 MB."
},
"updateKey": {
"message": "You cannot use this feature until you update your encryption key."
"encryptionKeyMigrationRequired": {
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
},
"editedFolder": {
"message": "Folder saved"

View File

@ -15,6 +15,7 @@ import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -210,4 +211,12 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
}
await super.submit(false);
}
protected override handleMigrateEncryptionKey(result: AuthResult): boolean {
if (!result.requiresEncryptionKeyMigration) {
return false;
}
this.router.navigate(["migrate-legacy-encryption"]);
return true;
}
}

View File

@ -0,0 +1,36 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<div class="tw-mt-12 tw-flex tw-justify-center">
<div class="tw-max-w-xl">
<h1 bitTypography="h1" class="tw-mb-4 tw-text-center">{{ "updateEncryptionKey" | i18n }}</h1>
<div
class="tw-block tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
>
<p>
{{ "updateEncryptionSchemeDesc" | i18n }}
<a
href="https://bitwarden.com/help/account-encryption-key/#rotate-your-encryption-key"
target="_blank"
rel="noopener"
>{{ "learnMore" | i18n }}</a
>
</p>
<bit-callout type="warning">{{ "updateEncryptionKeyWarning" | i18n }}</bit-callout>
<bit-form-field>
<bit-label>{{ "masterPass" | i18n }}</bit-label>
<input
id="masterPassword"
bitInput
type="password"
formControlName="masterPassword"
appAutofocus
/>
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field>
<button type="submit" bitButton bitFormButton buttonType="primary" block>
{{ "updateEncryptionKey" | i18n }}
</button>
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,82 @@
import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SharedModule } from "../../shared";
import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption.service";
// The master key was originally used to encrypt user data, before the user key was introduced.
// This component is used to migrate from the old encryption scheme to the new one.
@Component({
standalone: true,
imports: [SharedModule],
providers: [MigrateFromLegacyEncryptionService],
templateUrl: "migrate-legacy-encryption.component.html",
})
export class MigrateFromLegacyEncryptionComponent {
protected formGroup = new FormGroup({
masterPassword: new FormControl("", [Validators.required]),
});
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private migrationService: MigrateFromLegacyEncryptionService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private logService: LogService
) {}
submit = async () => {
this.formGroup.markAsTouched();
if (this.formGroup.invalid) {
return;
}
const hasUserKey = await this.cryptoService.hasUserKey();
if (hasUserKey) {
this.messagingService.send("logout");
throw new Error("User key already exists, cannot migrate legacy encryption.");
}
const masterPassword = this.formGroup.value.masterPassword;
try {
// Create new user key
const [newUserKey, masterKeyEncUserKey] = await this.migrationService.createNewUserKey(
masterPassword
);
// Update admin recover keys
await this.migrationService.updateAllAdminRecoveryKeys(masterPassword, newUserKey);
// Update emergency access
await this.migrationService.updateEmergencyAccesses(newUserKey);
// Update keys, folders, ciphers, and sends
await this.migrationService.updateKeysAndEncryptedData(
masterPassword,
newUserKey,
masterKeyEncUserKey
);
this.platformUtilsService.showToast(
"success",
this.i18nService.t("keyUpdated"),
this.i18nService.t("logBackInOthersToo"),
{ timeout: 15000 }
);
this.messagingService.send("logout");
} catch (e) {
this.logService.error(e);
throw e;
}
};
}

View File

@ -0,0 +1,345 @@
import { mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
import { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type";
import { EmergencyAccessUpdateRequest } from "@bitwarden/common/auth/models/request/emergency-access-update.request";
import { EmergencyAccessGranteeDetailsResponse } from "@bitwarden/common/auth/models/response/emergency-access.response";
import { EncryptionType, KdfType } from "@bitwarden/common/enums";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.response";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import {
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { Send } from "@bitwarden/common/tools/send/models/domain/send";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { Folder } from "@bitwarden/common/vault/models/domain/folder";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption.service";
describe("migrateFromLegacyEncryptionService", () => {
let migrateFromLegacyEncryptionService: MigrateFromLegacyEncryptionService;
const organizationService = mock<OrganizationService>();
const organizationApiService = mock<OrganizationApiService>();
const organizationUserService = mock<OrganizationUserService>();
const apiService = mock<ApiService>();
const encryptService = mock<EncryptService>();
const cryptoService = mock<CryptoService>();
const syncService = mock<SyncService>();
const cipherService = mock<CipherService>();
const folderService = mock<FolderService>();
const sendService = mock<SendService>();
const stateService = mock<StateService>();
let folderViews: BehaviorSubject<FolderView[]>;
let sends: BehaviorSubject<Send[]>;
beforeEach(() => {
jest.clearAllMocks();
migrateFromLegacyEncryptionService = new MigrateFromLegacyEncryptionService(
organizationService,
organizationApiService,
organizationUserService,
apiService,
cryptoService,
encryptService,
syncService,
cipherService,
folderService,
sendService,
stateService
);
});
it("instantiates", () => {
expect(migrateFromLegacyEncryptionService).not.toBeFalsy();
});
describe("createNewUserKey", () => {
it("validates master password and legacy user", async () => {
const mockMasterPassword = "mockMasterPassword";
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
const mockMasterKey = new SymmetricCryptoKey(mockRandomBytes) as MasterKey;
stateService.getEmail.mockResolvedValue("mockEmail");
stateService.getKdfType.mockResolvedValue(KdfType.PBKDF2_SHA256);
stateService.getKdfConfig.mockResolvedValue({ iterations: 100000 });
cryptoService.makeMasterKey.mockResolvedValue(mockMasterKey);
cryptoService.isLegacyUser.mockResolvedValue(false);
await expect(
migrateFromLegacyEncryptionService.createNewUserKey(mockMasterPassword)
).rejects.toThrowError("Invalid master password or user may not be legacy");
});
});
describe("updateKeysAndEncryptedData", () => {
let mockMasterPassword: string;
let mockUserKey: UserKey;
let mockEncUserKey: EncString;
beforeEach(() => {
mockMasterPassword = "mockMasterPassword";
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
mockEncUserKey = new EncString("mockEncUserKey");
const mockFolders = [createMockFolder("1", "Folder 1"), createMockFolder("2", "Folder 2")];
const mockCiphers = [createMockCipher("1", "Cipher 1"), createMockCipher("2", "Cipher 2")];
const mockSends = [createMockSend("1", "Send 1"), createMockSend("2", "Send 2")];
cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64) as CsprngArray);
folderViews = new BehaviorSubject<FolderView[]>(mockFolders);
folderService.folderViews$ = folderViews;
cipherService.getAllDecrypted.mockResolvedValue(mockCiphers);
sends = new BehaviorSubject<Send[]>(mockSends);
sendService.sends$ = sends;
encryptService.encrypt.mockImplementation((plainValue, userKey) => {
return Promise.resolve(
new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "Encrypted: " + plainValue)
);
});
folderService.encrypt.mockImplementation((folder, userKey) => {
const encryptedFolder = new Folder();
encryptedFolder.id = folder.id;
encryptedFolder.name = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"Encrypted: " + folder.name
);
return Promise.resolve(encryptedFolder);
});
cipherService.encrypt.mockImplementation((cipher, userKey) => {
const encryptedCipher = new Cipher();
encryptedCipher.id = cipher.id;
encryptedCipher.name = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"Encrypted: " + cipher.name
);
return Promise.resolve(encryptedCipher);
});
});
it("derives the master key in case it hasn't been set", async () => {
await migrateFromLegacyEncryptionService.updateKeysAndEncryptedData(
mockMasterPassword,
mockUserKey,
mockEncUserKey
);
expect(cryptoService.getOrDeriveMasterKey).toHaveBeenCalled();
});
it("syncs latest data", async () => {
await migrateFromLegacyEncryptionService.updateKeysAndEncryptedData(
mockMasterPassword,
mockUserKey,
mockEncUserKey
);
expect(syncService.fullSync).toHaveBeenCalledWith(true);
});
it("does not post new account data if sync fails", async () => {
syncService.fullSync.mockRejectedValueOnce(new Error("sync failed"));
await expect(
migrateFromLegacyEncryptionService.updateKeysAndEncryptedData(
mockMasterPassword,
mockUserKey,
mockEncUserKey
)
).rejects.toThrowError("sync failed");
expect(apiService.postAccountKey).not.toHaveBeenCalled();
});
it("does not post new account data if data retrieval fails", async () => {
(migrateFromLegacyEncryptionService as any).encryptCiphers = async () => {
throw new Error("Ciphers failed to be retrieved");
};
await expect(
migrateFromLegacyEncryptionService.updateKeysAndEncryptedData(
mockMasterPassword,
mockUserKey,
mockEncUserKey
)
).rejects.toThrowError("Ciphers failed to be retrieved");
expect(apiService.postAccountKey).not.toHaveBeenCalled();
});
});
describe("updateEmergencyAccesses", () => {
let mockUserKey: UserKey;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
const mockEmergencyAccess = {
data: [
createMockEmergencyAccess("0", "EA 0", EmergencyAccessStatusType.Invited),
createMockEmergencyAccess("1", "EA 1", EmergencyAccessStatusType.Accepted),
createMockEmergencyAccess("2", "EA 2", EmergencyAccessStatusType.Confirmed),
createMockEmergencyAccess("3", "EA 3", EmergencyAccessStatusType.RecoveryInitiated),
createMockEmergencyAccess("4", "EA 4", EmergencyAccessStatusType.RecoveryApproved),
],
} as ListResponse<EmergencyAccessGranteeDetailsResponse>;
apiService.getEmergencyAccessTrusted.mockResolvedValue(mockEmergencyAccess);
apiService.getUserPublicKey.mockResolvedValue({
userId: "mockUserId",
publicKey: "mockPublicKey",
} as UserKeyResponse);
cryptoService.rsaEncrypt.mockImplementation((plainValue, publicKey) => {
return Promise.resolve(
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "Encrypted: " + plainValue)
);
});
});
it("Only updates emergency accesses with allowed statuses", async () => {
await migrateFromLegacyEncryptionService.updateEmergencyAccesses(mockUserKey);
expect(apiService.putEmergencyAccess).not.toHaveBeenCalledWith(
"0",
expect.any(EmergencyAccessUpdateRequest)
);
expect(apiService.putEmergencyAccess).not.toHaveBeenCalledWith(
"1",
expect.any(EmergencyAccessUpdateRequest)
);
});
});
describe("updateAllAdminRecoveryKeys", () => {
let mockMasterPassword: string;
let mockUserKey: UserKey;
beforeEach(() => {
mockMasterPassword = "mockMasterPassword";
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
organizationService.getAll.mockResolvedValue([
createOrganization("1", "Org 1", true),
createOrganization("2", "Org 2", true),
createOrganization("3", "Org 3", false),
createOrganization("4", "Org 4", false),
]);
organizationApiService.getKeys.mockImplementation((orgId) => {
return Promise.resolve({
publicKey: orgId + "mockPublicKey",
privateKey: orgId + "mockPrivateKey",
} as OrganizationKeysResponse);
});
});
it("Only updates organizations that are enrolled in admin recovery", async () => {
await migrateFromLegacyEncryptionService.updateAllAdminRecoveryKeys(
mockMasterPassword,
mockUserKey
);
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment
).toHaveBeenCalledWith(
"1",
expect.any(String),
expect.any(OrganizationUserResetPasswordEnrollmentRequest)
);
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment
).toHaveBeenCalledWith(
"2",
expect.any(String),
expect.any(OrganizationUserResetPasswordEnrollmentRequest)
);
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment
).not.toHaveBeenCalledWith(
"3",
expect.any(String),
expect.any(OrganizationUserResetPasswordEnrollmentRequest)
);
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment
).not.toHaveBeenCalledWith(
"4",
expect.any(String),
expect.any(OrganizationUserResetPasswordEnrollmentRequest)
);
});
});
});
function createMockFolder(id: string, name: string): FolderView {
const folder = new FolderView();
folder.id = id;
folder.name = name;
return folder;
}
function createMockCipher(id: string, name: string): CipherView {
const cipher = new CipherView();
cipher.id = id;
cipher.name = name;
return cipher;
}
function createMockSend(id: string, name: string): Send {
const send = new Send();
send.id = id;
send.name = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, name);
return send;
}
function createMockEmergencyAccess(
id: string,
name: string,
status: EmergencyAccessStatusType
): EmergencyAccessGranteeDetailsResponse {
const emergencyAccess = new EmergencyAccessGranteeDetailsResponse({});
emergencyAccess.id = id;
emergencyAccess.name = name;
emergencyAccess.type = 0;
emergencyAccess.status = status;
return emergencyAccess;
}
function createOrganization(id: string, name: string, resetPasswordEnrolled: boolean) {
const org = new Organization();
org.id = id;
org.name = name;
org.resetPasswordEnrolled = resetPasswordEnrolled;
org.userId = "mockUserID";
return org;
}

View File

@ -0,0 +1,215 @@
import { Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type";
import { EmergencyAccessUpdateRequest } from "@bitwarden/common/auth/models/request/emergency-access-update.request";
import { UpdateKeyRequest } from "@bitwarden/common/models/request/update-key.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/cipher-with-id.request";
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
// TODO: PM-3797 - This service should be expanded and used for user key rotations in change-password.component.ts
@Injectable()
export class MigrateFromLegacyEncryptionService {
constructor(
private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService,
private apiService: ApiService,
private cryptoService: CryptoService,
private encryptService: EncryptService,
private syncService: SyncService,
private cipherService: CipherService,
private folderService: FolderService,
private sendService: SendService,
private stateService: StateService
) {}
/**
* Validates the master password and creates a new user key.
* @returns A new user key along with the encrypted version
*/
async createNewUserKey(masterPassword: string): Promise<[UserKey, EncString]> {
// Create master key to validate the master password
const masterKey = await this.cryptoService.makeMasterKey(
masterPassword,
await this.stateService.getEmail(),
await this.stateService.getKdfType(),
await this.stateService.getKdfConfig()
);
if (!masterKey) {
throw new Error("Invalid master password");
}
if (!(await this.cryptoService.isLegacyUser(masterKey))) {
throw new Error("Invalid master password or user may not be legacy");
}
// Set master key again in case it was lost (could be lost on refresh)
await this.cryptoService.setMasterKey(masterKey);
return await this.cryptoService.makeUserKey(masterKey);
}
/**
* Updates the user key, master key hash, private key, folders, ciphers, and sends
* on the server.
* @param masterPassword The master password
* @param newUserKey The new user key
* @param newEncUserKey The new encrypted user key
*/
async updateKeysAndEncryptedData(
masterPassword: string,
newUserKey: UserKey,
newEncUserKey: EncString
): Promise<void> {
// Create new request and add master key and hash
const request = new UpdateKeyRequest();
request.key = newEncUserKey.encryptedString;
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
masterPassword,
await this.cryptoService.getOrDeriveMasterKey(masterPassword)
);
// Sync before encrypting to make sure we have latest data
await this.syncService.fullSync(true);
request.privateKey = await this.encryptPrivateKey(newUserKey);
request.folders = await this.encryptFolders(newUserKey);
request.ciphers = await this.encryptCiphers(newUserKey);
request.sends = await this.encryptSends(newUserKey);
return this.apiService.postAccountKey(request);
}
/**
* Gets user's emergency access details from server and encrypts with new user key
* on the server.
* @param newUserKey The new user key
*/
async updateEmergencyAccesses(newUserKey: UserKey) {
const emergencyAccess = await this.apiService.getEmergencyAccessTrusted();
// Any Invited or Accepted requests won't have the key yet, so we don't need to update them
const allowedStatuses = new Set([
EmergencyAccessStatusType.Confirmed,
EmergencyAccessStatusType.RecoveryInitiated,
EmergencyAccessStatusType.RecoveryApproved,
]);
const filteredAccesses = emergencyAccess.data.filter((d) => allowedStatuses.has(d.status));
for (const details of filteredAccesses) {
// Get public key of grantee
const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
// Encrypt new user key with public key
const encryptedKey = await this.cryptoService.rsaEncrypt(newUserKey.key, publicKey);
const updateRequest = new EmergencyAccessUpdateRequest();
updateRequest.type = details.type;
updateRequest.waitTimeDays = details.waitTimeDays;
updateRequest.keyEncrypted = encryptedKey.encryptedString;
await this.apiService.putEmergencyAccess(details.id, updateRequest);
}
}
/** Updates all admin recovery keys on the server with the new user key
* @param masterPassword The user's master password
* @param newUserKey The new user key
*/
async updateAllAdminRecoveryKeys(masterPassword: string, newUserKey: UserKey) {
const allOrgs = await this.organizationService.getAll();
for (const org of allOrgs) {
// If not already enrolled, skip
if (!org.resetPasswordEnrolled) {
continue;
}
// Retrieve public key
const response = await this.organizationApiService.getKeys(org.id);
const publicKey = Utils.fromB64ToArray(response?.publicKey);
// Re-enroll - encrypt user key with organization public key
const encryptedKey = await this.cryptoService.rsaEncrypt(newUserKey.key, publicKey);
// Create/Execute request
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.resetPasswordKey = encryptedKey.encryptedString;
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
masterPassword,
await this.cryptoService.getOrDeriveMasterKey(masterPassword)
);
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
org.id,
org.userId,
request
);
}
}
private async encryptPrivateKey(newUserKey: UserKey): Promise<EncryptedString | null> {
const privateKey = await this.cryptoService.getPrivateKey();
if (!privateKey) {
return;
}
return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString;
}
private async encryptFolders(newUserKey: UserKey): Promise<FolderWithIdRequest[] | null> {
const folders = await firstValueFrom(this.folderService.folderViews$);
if (!folders) {
return;
}
return await Promise.all(
folders.map(async (folder) => {
const encryptedFolder = await this.folderService.encrypt(folder, newUserKey);
return new FolderWithIdRequest(encryptedFolder);
})
);
}
private async encryptCiphers(newUserKey: UserKey): Promise<CipherWithIdRequest[] | null> {
const ciphers = await this.cipherService.getAllDecrypted();
if (!ciphers) {
return;
}
return await Promise.all(
ciphers.map(async (cipher) => {
const encryptedCipher = await this.cipherService.encrypt(cipher, newUserKey);
return new CipherWithIdRequest(encryptedCipher);
})
);
}
private async encryptSends(newUserKey: UserKey): Promise<SendWithIdRequest[] | null> {
const sends = await firstValueFrom(this.sendService.sends$);
if (!sends) {
return;
}
return await Promise.all(
sends.map(async (send) => {
const sendKey = await this.encryptService.decryptToBytes(send.key, null);
send.key = (await this.encryptService.encrypt(sendKey, newUserKey)) ?? send.key;
return new SendWithIdRequest(send);
})
);
}
}

View File

@ -145,12 +145,6 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
}
async submit() {
const hasUserKey = await this.cryptoService.hasUserKey();
if (!hasUserKey) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("updateKey"));
return;
}
if (this.masterPasswordHint != null && this.masterPasswordHint == this.masterPassword) {
this.platformUtilsService.showToast(
"error",

View File

@ -9,6 +9,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@ -86,6 +87,14 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
);
}
protected override handleMigrateEncryptionKey(result: AuthResult): boolean {
if (!result.requiresEncryptionKeyMigration) {
return false;
}
this.router.navigate(["migrate-legacy-encryption"]);
return true;
}
goAfterLogIn = async () => {
this.loginService.clearValues();
const previousUrl = this.routerService.getPreviousUrl();

View File

@ -175,6 +175,13 @@ const routes: Routes = [
canActivate: [AuthGuard],
data: { titleId: "removeMasterPassword" },
},
{
path: "migrate-legacy-encryption",
loadComponent: () =>
import("./auth/migrate-encryption/migrate-legacy-encryption.component").then(
(mod) => mod.MigrateFromLegacyEncryptionComponent
),
},
],
},
{

View File

@ -42,12 +42,6 @@ export class ChangeEmailComponent implements OnInit {
}
async submit() {
const hasUserKey = await this.cryptoService.hasUserKey();
if (!hasUserKey) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("updateKey"));
return;
}
this.newEmail = this.newEmail.trim().toLowerCase();
if (!this.tokenSent) {
const request = new EmailTokenRequest();

View File

@ -46,11 +46,6 @@ export class ChangeKdfConfirmationComponent {
async submit() {
this.loading = true;
const hasUserKey = await this.cryptoService.hasUserKey();
if (!hasUserKey) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("updateKey"));
return;
}
try {
this.formPromise = this.makeKeyAndSaveAsync();

View File

@ -1,55 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="updateUserKeyTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h1 class="modal-title" id="updateUserKeyTitle">{{ "updateEncryptionKey" | i18n }}</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
{{ "updateEncryptionKeyShortDesc" | i18n }} {{ "updateEncryptionKeyDesc" | i18n }}
<a
href="https://bitwarden.com/help/account-encryption-key/#rotate-your-encryption-key"
target="_blank"
rel="noopener"
>{{ "learnMore" | i18n }}</a
>
</p>
<app-callout type="warning">{{ "updateEncryptionKeyWarning" | i18n }}</app-callout>
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="password"
name="MasterPasswordHash"
class="form-control"
[(ngModel)]="masterPassword"
required
appAutofocus
appInputVerbatim
/>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "updateEncryptionKey" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@ -1,108 +0,0 @@
import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UpdateKeyRequest } from "@bitwarden/common/models/request/update-key.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { CipherWithIdRequest } from "@bitwarden/common/vault//models/request/cipher-with-id.request";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
@Component({
selector: "app-update-key",
templateUrl: "update-key.component.html",
})
export class UpdateKeyComponent {
masterPassword: string;
formPromise: Promise<any>;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private syncService: SyncService,
private folderService: FolderService,
private cipherService: CipherService,
private logService: LogService
) {}
async submit() {
const hasUserKey = await this.cryptoService.hasUserKey();
if (hasUserKey) {
return;
}
if (this.masterPassword == null || this.masterPassword === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("masterPasswordRequired")
);
return;
}
try {
this.formPromise = this.makeRequest().then((request) => {
return this.apiService.postAccountKey(request);
});
await this.formPromise;
this.platformUtilsService.showToast(
"success",
this.i18nService.t("keyUpdated"),
this.i18nService.t("logBackInOthersToo"),
{ timeout: 15000 }
);
this.messagingService.send("logout");
} catch (e) {
this.logService.error(e);
}
}
private async makeRequest(): Promise<UpdateKeyRequest> {
const masterKey = await this.cryptoService.getMasterKey();
const newUserKey = await this.cryptoService.makeUserKey(masterKey);
const privateKey = await this.cryptoService.getPrivateKey();
let encPrivateKey: EncString = null;
if (privateKey != null) {
encPrivateKey = await this.cryptoService.encrypt(privateKey, newUserKey[0]);
}
const request = new UpdateKeyRequest();
request.privateKey = encPrivateKey != null ? encPrivateKey.encryptedString : null;
request.key = newUserKey[1].encryptedString;
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
this.masterPassword,
await this.cryptoService.getOrDeriveMasterKey(this.masterPassword)
);
await this.syncService.fullSync(true);
const folders = await firstValueFrom(this.folderService.folderViews$);
for (let i = 0; i < folders.length; i++) {
if (folders[i].id == null) {
continue;
}
const folder = await this.folderService.encrypt(folders[i], newUserKey[0]);
request.folders.push(new FolderWithIdRequest(folder));
}
const ciphers = await this.cipherService.getAllDecrypted();
for (let i = 0; i < ciphers.length; i++) {
if (ciphers[i].organizationId != null) {
continue;
}
const cipher = await this.cipherService.encrypt(ciphers[i], newUserKey[0]);
request.ciphers.push(new CipherWithIdRequest(cipher));
}
return request;
}
}

View File

@ -86,7 +86,6 @@ import { PurgeVaultComponent } from "../settings/purge-vault.component";
import { SecurityKeysComponent } from "../settings/security-keys.component";
import { SecurityComponent } from "../settings/security.component";
import { SettingsComponent } from "../settings/settings.component";
import { UpdateKeyComponent } from "../settings/update-key.component";
import { UpdateLicenseComponent } from "../settings/update-license.component";
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
import { GeneratorComponent } from "../tools/generator.component";
@ -216,7 +215,6 @@ import { SharedModule } from "./shared.module";
TwoFactorVerifyComponent,
TwoFactorWebAuthnComponent,
TwoFactorYubiKeyComponent,
UpdateKeyComponent,
UpdateLicenseComponent,
UpdatePasswordComponent,
UpdateTempPasswordComponent,
@ -319,7 +317,6 @@ import { SharedModule } from "./shared.module";
TwoFactorVerifyComponent,
TwoFactorWebAuthnComponent,
TwoFactorYubiKeyComponent,
UpdateKeyComponent,
UpdateLicenseComponent,
UpdatePasswordComponent,
UpdateTempPasswordComponent,

View File

@ -77,19 +77,6 @@
</div>
</div>
<div class="col-3">
<div class="card border-warning mb-4" *ngIf="showUpdateKey">
<div class="card-header bg-warning text-white">
<i class="bwi bwi-exclamation-triangle bwi-fw" aria-hidden="true"></i>
{{ "updateKeyTitle" | i18n }}
</div>
<div class="card-body">
<p>{{ "updateEncryptionKeyShortDesc" | i18n }}</p>
<button class="btn btn-block btn-outline-secondary" type="button" (click)="updateKey()">
{{ "updateEncryptionKey" | i18n }}
</button>
</div>
</div>
<app-low-kdf class="d-block mb-4" *ngIf="showLowKdf"> </app-low-kdf>
<app-verify-email

View File

@ -60,7 +60,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { DialogService, Icons } from "@bitwarden/components";
import { UpdateKeyComponent } from "../../settings/update-key.component";
import { CollectionDialogAction, openCollectionDialog } from "../components/collection-dialog";
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
import { getNestedCollectionTree } from "../utils/collection-utils";
@ -114,12 +113,9 @@ export class VaultComponent implements OnInit, OnDestroy {
@ViewChild("share", { read: ViewContainerRef, static: true }) shareModalRef: ViewContainerRef;
@ViewChild("collectionsModal", { read: ViewContainerRef, static: true })
collectionsModalRef: ViewContainerRef;
@ViewChild("updateKeyTemplate", { read: ViewContainerRef, static: true })
updateKeyModalRef: ViewContainerRef;
showVerifyEmail = false;
showBrowserOutdated = false;
showUpdateKey = false;
showPremiumCallout = false;
showLowKdf = false;
trashCleanupWarning: string = null;
@ -197,7 +193,6 @@ export class VaultComponent implements OnInit, OnDestroy {
const canAccessPremium = await this.stateService.getCanAccessPremium();
this.showPremiumCallout =
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
this.showUpdateKey = !(await this.cryptoService.hasUserKey());
const cipherId = getCipherIdFromParams(params);
if (!cipherId) {
@ -405,11 +400,7 @@ export class VaultComponent implements OnInit, OnDestroy {
get isShowingCards() {
return (
this.showBrowserOutdated ||
this.showPremiumCallout ||
this.showUpdateKey ||
this.showVerifyEmail ||
this.showLowKdf
this.showBrowserOutdated || this.showPremiumCallout || this.showVerifyEmail || this.showLowKdf
);
}
@ -865,10 +856,6 @@ export class VaultComponent implements OnInit, OnDestroy {
: this.cipherService.softDeleteWithServer(id);
}
async updateKey() {
await this.modalService.openViewRef(UpdateKeyComponent, this.updateKeyModalRef);
}
async isLowKdfIteration() {
const kdfType = await this.stateService.getKdfType();
const kdfOptions = await this.stateService.getKdfConfig();

View File

@ -508,9 +508,6 @@
"maxFileSize": {
"message": "Maximum file size is 500 MB."
},
"updateKey": {
"message": "You cannot use this feature until you update your encryption key."
},
"addedItem": {
"message": "Item added"
},
@ -3476,17 +3473,11 @@
"keyUpdated": {
"message": "Key updated"
},
"updateKeyTitle": {
"message": "Update key"
},
"updateEncryptionKey": {
"message": "Update encryption key"
},
"updateEncryptionKeyShortDesc": {
"message": "You are currently using an outdated encryption scheme."
},
"updateEncryptionKeyDesc": {
"message": "We've moved to larger encryption keys that provide better security and access to newer features. Updating your encryption key is quick and easy. Just type your master password below. This update will eventually become mandatory."
"updateEncryptionSchemeDesc": {
"message": "We've changed the encryption scheme to provide better security. Update your encryption key now by entering your master password below."
},
"updateEncryptionKeyWarning": {
"message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed."

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

@ -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

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