mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-26 12:25:20 +01:00
[PM-1339] Allow Rotating Device Keys (#5806)
* Merge remote-tracking branch 'origin/feature/trusted-device-encryption' into Auth/pm-1339/rotate-device-keys * Implement Rotation of Current Device Keys - Detects if you are on a trusted device - Will rotate your keys of only this device - Allows you to still log in through SSO and decrypt your vault because the device is still trusted * Address PR Feedback * Move Files to Auth Ownership
This commit is contained in:
parent
3d7a0bb151
commit
2f855dd37a
@ -4,8 +4,8 @@ import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
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 { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { AvatarUpdateService as AvatarUpdateServiceAbstraction } from "@bitwarden/common/abstractions/account/avatar-update.service";
|
||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
|
||||
@ -18,6 +17,7 @@ import { PolicyApiService } from "@bitwarden/common/admin-console/services/polic
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
@ -25,6 +25,7 @@ import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/ab
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
@ -62,7 +63,6 @@ import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/we
|
||||
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
|
||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/services/devices/devices-api.service.implementation";
|
||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||
import { NotificationsService } from "@bitwarden/common/services/notifications.service";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/services/devices/devices-api.service.implementation";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
|
||||
import {
|
||||
ApiServiceInitOptions,
|
||||
|
@ -4,7 +4,6 @@ import * as path from "path";
|
||||
import * as program from "commander";
|
||||
import * as jsdom from "jsdom";
|
||||
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
@ -14,8 +13,10 @@ import { PolicyApiService } from "@bitwarden/common/admin-console/services/polic
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
@ -37,7 +38,6 @@ import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-m
|
||||
import { StateMigrationService } from "@bitwarden/common/platform/services/state-migration.service";
|
||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/services/devices/devices-api.service.implementation";
|
||||
import { OrganizationUserServiceImplementation } from "@bitwarden/common/services/organization-user/organization-user.service.implementation";
|
||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||
import { SettingsService } from "@bitwarden/common/services/settings.service";
|
||||
|
@ -7,8 +7,8 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components
|
||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
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 { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
|
@ -6,7 +6,6 @@ import { first } from "rxjs/operators";
|
||||
|
||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
|
||||
@ -14,6 +13,7 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.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 { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
|
@ -11,6 +11,7 @@ import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/commo
|
||||
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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type";
|
||||
@ -69,7 +70,8 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
dialogService: DialogServiceAbstraction,
|
||||
private userVerificationService: UserVerificationService
|
||||
private userVerificationService: UserVerificationService,
|
||||
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
@ -220,15 +222,15 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||
}
|
||||
|
||||
private async updateKey(masterKey: MasterKey, masterPasswordHash: string) {
|
||||
const userKey = await this.cryptoService.makeUserKey(masterKey);
|
||||
const privateKey = await this.cryptoService.getPrivateKey();
|
||||
const [newUserKey, masterKeyEncUserKey] = await this.cryptoService.makeUserKey(masterKey);
|
||||
const userPrivateKey = await this.cryptoService.getPrivateKey();
|
||||
let encPrivateKey: EncString = null;
|
||||
if (privateKey != null) {
|
||||
encPrivateKey = await this.cryptoService.encrypt(privateKey, userKey[0]);
|
||||
if (userPrivateKey != null) {
|
||||
encPrivateKey = await this.cryptoService.encrypt(userPrivateKey, newUserKey);
|
||||
}
|
||||
const request = new UpdateKeyRequest();
|
||||
request.privateKey = encPrivateKey != null ? encPrivateKey.encryptedString : null;
|
||||
request.key = userKey[1].encryptedString;
|
||||
request.key = masterKeyEncUserKey.encryptedString;
|
||||
request.masterPasswordHash = masterPasswordHash;
|
||||
|
||||
const folders = await firstValueFrom(this.folderService.folderViews$);
|
||||
@ -236,7 +238,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||
if (folders[i].id == null) {
|
||||
continue;
|
||||
}
|
||||
const folder = await this.folderService.encrypt(folders[i], userKey[0]);
|
||||
const folder = await this.folderService.encrypt(folders[i], newUserKey);
|
||||
request.folders.push(new FolderWithIdRequest(folder));
|
||||
}
|
||||
|
||||
@ -246,7 +248,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cipher = await this.cipherService.encrypt(ciphers[i], userKey[0]);
|
||||
const cipher = await this.cipherService.encrypt(ciphers[i], newUserKey);
|
||||
request.ciphers.push(new CipherWithIdRequest(cipher));
|
||||
}
|
||||
|
||||
@ -254,16 +256,18 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||
await Promise.all(
|
||||
sends.map(async (send) => {
|
||||
const sendKey = await this.cryptoService.decryptToBytes(send.key, null);
|
||||
send.key = (await this.cryptoService.encrypt(sendKey, userKey[0])) ?? send.key;
|
||||
send.key = (await this.cryptoService.encrypt(sendKey, newUserKey)) ?? send.key;
|
||||
request.sends.push(new SendWithIdRequest(send));
|
||||
})
|
||||
);
|
||||
|
||||
await this.deviceTrustCryptoService.rotateDevicesTrust(newUserKey, masterPasswordHash);
|
||||
|
||||
await this.apiService.postAccountKey(request);
|
||||
|
||||
await this.updateEmergencyAccesses(userKey[0]);
|
||||
await this.updateEmergencyAccesses(newUserKey);
|
||||
|
||||
await this.updateAllResetPasswordKeys(userKey[0], masterPasswordHash);
|
||||
await this.updateAllResetPasswordKeys(newUserKey, masterPasswordHash);
|
||||
}
|
||||
|
||||
private async updateEmergencyAccesses(encKey: SymmetricCryptoKey) {
|
@ -1,9 +1,9 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { ChangePasswordComponent } from "../auth/settings/change-password.component";
|
||||
import { TwoFactorSetupComponent } from "../auth/settings/two-factor-setup.component";
|
||||
|
||||
import { ChangePasswordComponent } from "./change-password.component";
|
||||
import { SecurityKeysComponent } from "./security-keys.component";
|
||||
import { SecurityComponent } from "./security.component";
|
||||
|
||||
|
@ -26,6 +26,7 @@ import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component"
|
||||
import { RegisterFormModule } from "../auth/register-form/register-form.module";
|
||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { ChangePasswordComponent } from "../auth/settings/change-password.component";
|
||||
import { DeauthorizeSessionsComponent } from "../auth/settings/deauthorize-sessions.component";
|
||||
import { EmergencyAccessAddEditComponent } from "../auth/settings/emergency-access/emergency-access-add-edit.component";
|
||||
import { EmergencyAccessAttachmentsComponent } from "../auth/settings/emergency-access/emergency-access-attachments.component";
|
||||
@ -75,7 +76,6 @@ import { ApiKeyComponent } from "../settings/api-key.component";
|
||||
import { ChangeAvatarComponent } from "../settings/change-avatar.component";
|
||||
import { ChangeEmailComponent } from "../settings/change-email.component";
|
||||
import { ChangeKdfModule } from "../settings/change-kdf/change-kdf.module";
|
||||
import { ChangePasswordComponent } from "../settings/change-password.component";
|
||||
import { DeleteAccountComponent } from "../settings/delete-account.component";
|
||||
import { DomainRulesComponent } from "../settings/domain-rules.component";
|
||||
import { LowKdfComponent } from "../settings/low-kdf.component";
|
||||
|
@ -3,8 +3,8 @@ import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { take } from "rxjs/operators";
|
||||
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
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 { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
|
||||
|
@ -4,7 +4,6 @@ import { AvatarUpdateService as AccountUpdateServiceAbstraction } from "@bitward
|
||||
import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/abstractions/anonymousHub.service";
|
||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
@ -43,6 +42,7 @@ import {
|
||||
} from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
@ -53,6 +53,7 @@ import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/services/login.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
@ -97,7 +98,6 @@ import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-u
|
||||
import { AnonymousHubService } from "@bitwarden/common/services/anonymousHub.service";
|
||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/services/devices/devices-api.service.implementation";
|
||||
import { DevicesServiceImplementation } from "@bitwarden/common/services/devices/devices.service.implementation";
|
||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
|
||||
import { DeviceResponse } from "./responses/device.response";
|
||||
|
||||
export abstract class DevicesApiServiceAbstraction {
|
||||
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
|
||||
|
||||
getDeviceByIdentifier: (deviceIdentifier: string) => Promise<DeviceResponse>;
|
||||
|
||||
getDevices: () => Promise<ListResponse<DeviceResponse>>;
|
||||
|
||||
updateTrustedDeviceKeys: (
|
||||
deviceIdentifier: string,
|
||||
devicePublicKeyEncryptedUserKey: string,
|
||||
userKeyEncryptedDevicePublicKey: string,
|
||||
deviceKeyEncryptedDevicePrivateKey: string
|
||||
) => Promise<DeviceResponse>;
|
||||
}
|
@ -17,4 +17,5 @@ export abstract class DeviceTrustCryptoServiceAbstraction {
|
||||
encryptedUserKey: EncString,
|
||||
deviceKey?: DeviceKey
|
||||
) => Promise<UserKey | null>;
|
||||
rotateDevicesTrust: (newUserKey: UserKey, masterPasswordHash: string) => Promise<void>;
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
|
||||
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
|
||||
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
|
||||
|
||||
export abstract class DevicesApiServiceAbstraction {
|
||||
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
|
||||
|
||||
getDeviceByIdentifier: (deviceIdentifier: string) => Promise<DeviceResponse>;
|
||||
|
||||
getDevices: () => Promise<ListResponse<DeviceResponse>>;
|
||||
|
||||
updateTrustedDeviceKeys: (
|
||||
deviceIdentifier: string,
|
||||
devicePublicKeyEncryptedUserKey: string,
|
||||
userKeyEncryptedDevicePublicKey: string,
|
||||
deviceKeyEncryptedDevicePrivateKey: string
|
||||
) => Promise<DeviceResponse>;
|
||||
|
||||
updateTrust: (updateDevicesTrustRequestModel: UpdateDevicesTrustRequest) => Promise<void>;
|
||||
|
||||
getDeviceKeys: (
|
||||
deviceIdentifier: string,
|
||||
secretVerificationRequest: SecretVerificationRequest
|
||||
) => Promise<ProtectedDeviceResponse>;
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { SecretVerificationRequest } from "./secret-verification.request";
|
||||
|
||||
export class UpdateDevicesTrustRequest extends SecretVerificationRequest {
|
||||
currentDevice: DeviceKeysUpdateRequest;
|
||||
otherDevices: OtherDeviceKeysUpdateRequest[];
|
||||
}
|
||||
|
||||
export class DeviceKeysUpdateRequest {
|
||||
encryptedPublicKey: string;
|
||||
encryptedUserKey: string;
|
||||
}
|
||||
|
||||
export class OtherDeviceKeysUpdateRequest extends DeviceKeysUpdateRequest {
|
||||
id: string;
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { DeviceType } from "../../../enums";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
|
||||
export class ProtectedDeviceResponse extends BaseResponse {
|
||||
constructor(response: Jsonify<ProtectedDeviceResponse>) {
|
||||
super(response);
|
||||
this.id = this.getResponseProperty("id");
|
||||
this.name = this.getResponseProperty("name");
|
||||
this.identifier = this.getResponseProperty("identifier");
|
||||
this.type = this.getResponseProperty("type");
|
||||
this.creationDate = new Date(this.getResponseProperty("creationDate"));
|
||||
if (response.encryptedUserKey) {
|
||||
this.encryptedUserKey = new EncString(this.getResponseProperty("encryptedUserKey"));
|
||||
}
|
||||
if (response.encryptedPublicKey) {
|
||||
this.encryptedPublicKey = new EncString(this.getResponseProperty("encryptedPublicKey"));
|
||||
}
|
||||
}
|
||||
|
||||
id: string;
|
||||
name: string;
|
||||
type: DeviceType;
|
||||
identifier: string;
|
||||
creationDate: Date;
|
||||
/**
|
||||
* Intended to be the users symmetric key that is encrypted in some form, the current way to encrypt this is with
|
||||
* the devices public key.
|
||||
*/
|
||||
encryptedUserKey: EncString;
|
||||
/**
|
||||
* Intended to be the public key that was generated for a device upon trust and encrypted. Currenly encrypted using
|
||||
* a users symmetric key so that when trusted and unlocked a user can decrypt the public key for all their devices.
|
||||
* This enabled a user to rotate the keys for all of their devices.
|
||||
*/
|
||||
encryptedPublicKey: EncString;
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import { DevicesApiServiceAbstraction } from "../../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
||||
@ -13,6 +12,12 @@ import {
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
|
||||
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
|
||||
import {
|
||||
DeviceKeysUpdateRequest,
|
||||
UpdateDevicesTrustRequest,
|
||||
} from "../models/request/update-devices-trust.request";
|
||||
|
||||
export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction {
|
||||
constructor(
|
||||
@ -82,6 +87,60 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
|
||||
return deviceResponse;
|
||||
}
|
||||
|
||||
async rotateDevicesTrust(newUserKey: UserKey, masterPasswordHash: string): Promise<void> {
|
||||
const currentDeviceKey = await this.getDeviceKey();
|
||||
if (currentDeviceKey == null) {
|
||||
// If the current device doesn't have a device key available to it, then we can't
|
||||
// rotate any trust at all, so early return.
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point of rotating their keys, they should still have their old user key in state
|
||||
const oldUserKey = await this.stateService.getUserKey();
|
||||
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const secretVerificationRequest = new SecretVerificationRequest();
|
||||
secretVerificationRequest.masterPasswordHash = masterPasswordHash;
|
||||
|
||||
// Get the keys that are used in rotating a devices keys from the server
|
||||
const currentDeviceKeys = await this.devicesApiService.getDeviceKeys(
|
||||
deviceIdentifier,
|
||||
secretVerificationRequest
|
||||
);
|
||||
|
||||
// Decrypt the existing device public key with the old user key
|
||||
const decryptedDevicePublicKey = await this.encryptService.decryptToBytes(
|
||||
currentDeviceKeys.encryptedPublicKey,
|
||||
oldUserKey
|
||||
);
|
||||
|
||||
// Encrypt the brand new user key with the now-decrypted public key for the device
|
||||
const encryptedNewUserKey = await this.cryptoService.rsaEncrypt(
|
||||
newUserKey.key,
|
||||
decryptedDevicePublicKey
|
||||
);
|
||||
|
||||
// Re-encrypt the device public key with the new user key
|
||||
const encryptedDevicePublicKey = await this.encryptService.encrypt(
|
||||
decryptedDevicePublicKey,
|
||||
newUserKey
|
||||
);
|
||||
|
||||
const currentDeviceUpdateRequest = new DeviceKeysUpdateRequest();
|
||||
currentDeviceUpdateRequest.encryptedUserKey = encryptedNewUserKey.encryptedString;
|
||||
currentDeviceUpdateRequest.encryptedPublicKey = encryptedDevicePublicKey.encryptedString;
|
||||
|
||||
// TODO: For device management, allow this method to take an array of device ids that can be looped over and individually rotated
|
||||
// then it can be added to trustRequest.otherDevices.
|
||||
|
||||
const trustRequest = new UpdateDevicesTrustRequest();
|
||||
trustRequest.masterPasswordHash = masterPasswordHash;
|
||||
trustRequest.currentDevice = currentDeviceUpdateRequest;
|
||||
trustRequest.otherDevices = [];
|
||||
|
||||
await this.devicesApiService.updateTrust(trustRequest);
|
||||
}
|
||||
|
||||
async getDeviceKey(): Promise<DeviceKey> {
|
||||
return await this.stateService.getDeviceKey();
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { mock, mockReset } from "jest-mock-extended";
|
||||
import { matches, mock, mockReset } from "jest-mock-extended";
|
||||
|
||||
import { DevicesApiServiceAbstraction } from "../../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { DeviceType } from "../../enums";
|
||||
import { EncryptionType } from "../../enums/encryption-type.enum";
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
||||
@ -15,6 +15,9 @@ import {
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CryptoService } from "../../platform/services/crypto.service";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
|
||||
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
|
||||
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
|
||||
|
||||
import { DeviceTrustCryptoService } from "./device-trust-crypto.service.implementation";
|
||||
|
||||
@ -454,5 +457,113 @@ describe("deviceTrustCryptoService", () => {
|
||||
expect(setDeviceKeySpy).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rotateDevicesTrust", () => {
|
||||
let fakeNewUserKey: UserKey = null;
|
||||
|
||||
const FakeNewUserKeyMarker = 1;
|
||||
const FakeOldUserKeyMarker = 5;
|
||||
const FakeDecryptedPublicKeyMarker = 17;
|
||||
|
||||
beforeEach(() => {
|
||||
const fakeNewUserKeyData = new Uint8Array(new ArrayBuffer(64));
|
||||
fakeNewUserKeyData.fill(FakeNewUserKeyMarker, 0, 1);
|
||||
fakeNewUserKey = new SymmetricCryptoKey(fakeNewUserKeyData) as UserKey;
|
||||
});
|
||||
|
||||
it("does an early exit when the current device is not a trusted device", async () => {
|
||||
stateService.getDeviceKey.mockResolvedValue(null);
|
||||
|
||||
await deviceTrustCryptoService.rotateDevicesTrust(fakeNewUserKey, "");
|
||||
|
||||
expect(devicesApiService.updateTrust).not.toBeCalled();
|
||||
});
|
||||
|
||||
describe("is on a trusted device", () => {
|
||||
beforeEach(() => {
|
||||
stateService.getDeviceKey.mockResolvedValue(
|
||||
new SymmetricCryptoKey(new ArrayBuffer(deviceKeyBytesLength)) as DeviceKey
|
||||
);
|
||||
});
|
||||
|
||||
it("rotates current device keys and calls api service when the current device is trusted", async () => {
|
||||
const currentEncryptedPublicKey = new EncString("2.cHVibGlj|cHVibGlj|cHVibGlj");
|
||||
const currentEncryptedUserKey = new EncString("4.dXNlcg==");
|
||||
|
||||
const fakeOldUserKeyData = new Uint8Array(new ArrayBuffer(64));
|
||||
// Fill the first byte with something identifiable
|
||||
fakeOldUserKeyData.fill(FakeOldUserKeyMarker, 0, 1);
|
||||
|
||||
// Mock the retrieval of a user key that differs from the new one passed into the method
|
||||
stateService.getUserKey.mockResolvedValue(
|
||||
new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey
|
||||
);
|
||||
|
||||
appIdService.getAppId.mockResolvedValue("test_device_identifier");
|
||||
|
||||
devicesApiService.getDeviceKeys.mockImplementation((deviceIdentifier, secretRequest) => {
|
||||
if (
|
||||
deviceIdentifier !== "test_device_identifier" ||
|
||||
secretRequest.masterPasswordHash !== "my_password_hash"
|
||||
) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return Promise.resolve(
|
||||
new ProtectedDeviceResponse({
|
||||
id: "",
|
||||
creationDate: "",
|
||||
identifier: "test_device_identifier",
|
||||
name: "Firefox",
|
||||
type: DeviceType.FirefoxBrowser,
|
||||
encryptedPublicKey: currentEncryptedPublicKey.encryptedString,
|
||||
encryptedUserKey: currentEncryptedUserKey.encryptedString,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Mock the decryption of the public key with the old user key
|
||||
encryptService.decryptToBytes.mockImplementationOnce((_encValue, privateKeyValue) => {
|
||||
expect(privateKeyValue.key.byteLength).toBe(64);
|
||||
expect(new Uint8Array(privateKeyValue.key)[0]).toBe(FakeOldUserKeyMarker);
|
||||
const data = new Uint8Array(new ArrayBuffer(250));
|
||||
data.fill(FakeDecryptedPublicKeyMarker, 0, 1);
|
||||
return Promise.resolve(data.buffer);
|
||||
});
|
||||
|
||||
// Mock the encryption of the new user key with the decrypted public key
|
||||
cryptoService.rsaEncrypt.mockImplementationOnce((data, publicKey) => {
|
||||
expect(data.byteLength).toBe(64); // New key should also be 64 bytes
|
||||
expect(new Uint8Array(data)[0]).toBe(FakeNewUserKeyMarker); // New key should have the first byte be '1';
|
||||
|
||||
expect(new Uint8Array(publicKey)[0]).toBe(FakeDecryptedPublicKeyMarker);
|
||||
return Promise.resolve(new EncString("4.ZW5jcnlwdGVkdXNlcg=="));
|
||||
});
|
||||
|
||||
// Mock the reencryption of the device public key with the new user key
|
||||
encryptService.encrypt.mockImplementationOnce((plainValue, key) => {
|
||||
expect(plainValue).toBeInstanceOf(ArrayBuffer);
|
||||
expect(new Uint8Array(plainValue as ArrayBuffer)[0]).toBe(FakeDecryptedPublicKeyMarker);
|
||||
|
||||
expect(new Uint8Array(key.key)[0]).toBe(FakeNewUserKeyMarker);
|
||||
return Promise.resolve(
|
||||
new EncString("2.ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj")
|
||||
);
|
||||
});
|
||||
|
||||
await deviceTrustCryptoService.rotateDevicesTrust(fakeNewUserKey, "my_password_hash");
|
||||
|
||||
expect(devicesApiService.updateTrust).toBeCalledWith(
|
||||
matches((updateTrustModel: UpdateDevicesTrustRequest) => {
|
||||
return (
|
||||
updateTrustModel.currentDevice.encryptedPublicKey ===
|
||||
"2.ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj" &&
|
||||
updateTrustModel.currentDevice.encryptedUserKey === "4.ZW5jcnlwdGVkdXNlcg=="
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { DevicesApiServiceAbstraction } from "../../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
|
||||
import { TrustedDeviceKeysRequest } from "./requests/trusted-device-keys.request";
|
||||
import { TrustedDeviceKeysRequest } from "../../services/devices/requests/trusted-device-keys.request";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
|
||||
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
|
||||
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
|
||||
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
|
||||
|
||||
export class DevicesApiServiceImplementation implements DevicesApiServiceAbstraction {
|
||||
constructor(private apiService: ApiService) {}
|
||||
@ -67,4 +69,28 @@ export class DevicesApiServiceImplementation implements DevicesApiServiceAbstrac
|
||||
|
||||
return new DeviceResponse(result);
|
||||
}
|
||||
|
||||
async updateTrust(updateDevicesTrustRequestModel: UpdateDevicesTrustRequest): Promise<void> {
|
||||
await this.apiService.send(
|
||||
"POST",
|
||||
"devices/update-trust",
|
||||
updateDevicesTrustRequestModel,
|
||||
true,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
async getDeviceKeys(
|
||||
deviceIdentifier: string,
|
||||
secretVerificationRequest: SecretVerificationRequest
|
||||
): Promise<ProtectedDeviceResponse> {
|
||||
const result = await this.apiService.send(
|
||||
"POST",
|
||||
`/devices/${deviceIdentifier}/retrieve-keys`,
|
||||
secretVerificationRequest,
|
||||
true,
|
||||
true
|
||||
);
|
||||
return new ProtectedDeviceResponse(result);
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { Observable, defer, map } from "rxjs";
|
||||
|
||||
import { DevicesApiServiceAbstraction } from "../../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DevicesServiceAbstraction } from "../../abstractions/devices/devices.service.abstraction";
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { DeviceView } from "../../abstractions/devices/views/device.view";
|
||||
import { DevicesApiServiceAbstraction } from "../../auth/abstractions/devices-api.service.abstraction";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user