mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 01:03:35 +00:00
Merge branch 'main' into auth/pm-8111/browser-refresh-login-component
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
||||
|
||||
import { CollectionAccessSelectionView, CollectionAdminView } from "../models";
|
||||
|
||||
export abstract class CollectionAdminService {
|
||||
getAll: (organizationId: string) => Promise<CollectionAdminView[]>;
|
||||
get: (organizationId: string, collectionId: string) => Promise<CollectionAdminView | undefined>;
|
||||
save: (collection: CollectionAdminView) => Promise<CollectionDetailsResponse>;
|
||||
delete: (organizationId: string, collectionId: string) => Promise<void>;
|
||||
bulkAssignAccess: (
|
||||
organizationId: string,
|
||||
collectionIds: string[],
|
||||
users: CollectionAccessSelectionView[],
|
||||
groups: CollectionAccessSelectionView[],
|
||||
) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./collection-admin.service";
|
||||
3
libs/admin-console/src/common/collections/index.ts
Normal file
3
libs/admin-console/src/common/collections/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./abstractions";
|
||||
export * from "./models";
|
||||
export * from "./services";
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
|
||||
|
||||
export class BulkCollectionAccessRequest {
|
||||
collectionIds: string[];
|
||||
users: SelectionReadOnlyRequest[];
|
||||
groups: SelectionReadOnlyRequest[];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { View } from "@bitwarden/common/models/view/view";
|
||||
|
||||
interface SelectionResponseLike {
|
||||
id: string;
|
||||
readOnly: boolean;
|
||||
hidePasswords: boolean;
|
||||
manage: boolean;
|
||||
}
|
||||
|
||||
export class CollectionAccessSelectionView extends View {
|
||||
readonly id: string;
|
||||
readonly readOnly: boolean;
|
||||
readonly hidePasswords: boolean;
|
||||
readonly manage: boolean;
|
||||
|
||||
constructor(response?: SelectionResponseLike) {
|
||||
super();
|
||||
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.id = response.id;
|
||||
this.readOnly = response.readOnly;
|
||||
this.hidePasswords = response.hidePasswords;
|
||||
this.manage = response.manage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/models/response/collection.response";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
||||
import { CollectionAccessSelectionView } from "../models";
|
||||
|
||||
export const Unassigned = "unassigned";
|
||||
|
||||
export class CollectionAdminView extends CollectionView {
|
||||
groups: CollectionAccessSelectionView[] = [];
|
||||
users: CollectionAccessSelectionView[] = [];
|
||||
|
||||
/**
|
||||
* Flag indicating the collection has no active user or group assigned to it with CanManage permissions
|
||||
* In this case, the collection can be managed by admins/owners or custom users with appropriate permissions
|
||||
*/
|
||||
unmanaged: boolean;
|
||||
|
||||
/**
|
||||
* Flag indicating the user has been explicitly assigned to this Collection
|
||||
*/
|
||||
assigned: boolean;
|
||||
|
||||
constructor(response?: CollectionAccessDetailsResponse) {
|
||||
super(response);
|
||||
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.groups = response.groups
|
||||
? response.groups.map((g) => new CollectionAccessSelectionView(g))
|
||||
: [];
|
||||
|
||||
this.users = response.users
|
||||
? response.users.map((g) => new CollectionAccessSelectionView(g))
|
||||
: [];
|
||||
|
||||
this.assigned = response.assigned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user can edit a collection (including user and group access) from the Admin Console.
|
||||
*/
|
||||
override canEdit(org: Organization): boolean {
|
||||
return (
|
||||
org?.canEditAnyCollection ||
|
||||
(this.unmanaged && org?.canEditUnmanagedCollections) ||
|
||||
super.canEdit(org)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user can delete a collection from the Admin Console.
|
||||
*/
|
||||
override canDelete(org: Organization): boolean {
|
||||
return org?.canDeleteAnyCollection || super.canDelete(org);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user can modify user access to this collection
|
||||
*/
|
||||
canEditUserAccess(org: Organization): boolean {
|
||||
return (
|
||||
(org.permissions.manageUsers && org.allowAdminAccessToAllCollectionItems) || this.canEdit(org)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user can modify group access to this collection
|
||||
*/
|
||||
canEditGroupAccess(org: Organization): boolean {
|
||||
return (
|
||||
(org.permissions.manageGroups && org.allowAdminAccessToAllCollectionItems) ||
|
||||
this.canEdit(org)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user can view collection info and access in a read-only state from the Admin Console
|
||||
*/
|
||||
override canViewCollectionInfo(org: Organization | undefined): boolean {
|
||||
if (this.isUnassignedCollection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.manage || org?.isAdmin || org?.permissions.editAnyCollection;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this collection represents the pseudo "Unassigned" collection
|
||||
* This is different from the "unmanaged" flag, which indicates that no users or groups have access to the collection
|
||||
*/
|
||||
get isUnassignedCollection() {
|
||||
return this.id === Unassigned;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./bulk-collection-access.request";
|
||||
export * from "./collection-access-selection.view";
|
||||
export * from "./collection-admin.view";
|
||||
@@ -0,0 +1,169 @@
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
||||
import { CollectionRequest } from "@bitwarden/common/vault/models/request/collection.request";
|
||||
import {
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from "@bitwarden/common/vault/models/response/collection.response";
|
||||
|
||||
import { CollectionAdminService } from "../abstractions";
|
||||
import {
|
||||
BulkCollectionAccessRequest,
|
||||
CollectionAccessSelectionView,
|
||||
CollectionAdminView,
|
||||
} from "../models";
|
||||
|
||||
export class DefaultCollectionAdminService implements CollectionAdminService {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private cryptoService: CryptoService,
|
||||
private encryptService: EncryptService,
|
||||
private collectionService: CollectionService,
|
||||
) {}
|
||||
|
||||
async getAll(organizationId: string): Promise<CollectionAdminView[]> {
|
||||
const collectionResponse =
|
||||
await this.apiService.getManyCollectionsWithAccessDetails(organizationId);
|
||||
|
||||
if (collectionResponse?.data == null || collectionResponse.data.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await this.decryptMany(organizationId, collectionResponse.data);
|
||||
}
|
||||
|
||||
async get(
|
||||
organizationId: string,
|
||||
collectionId: string,
|
||||
): Promise<CollectionAdminView | undefined> {
|
||||
const collectionResponse = await this.apiService.getCollectionAccessDetails(
|
||||
organizationId,
|
||||
collectionId,
|
||||
);
|
||||
|
||||
if (collectionResponse == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [view] = await this.decryptMany(organizationId, [collectionResponse]);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
async save(collection: CollectionAdminView): Promise<CollectionDetailsResponse> {
|
||||
const request = await this.encrypt(collection);
|
||||
|
||||
let response: CollectionDetailsResponse;
|
||||
if (collection.id == null) {
|
||||
response = await this.apiService.postCollection(collection.organizationId, request);
|
||||
collection.id = response.id;
|
||||
} else {
|
||||
response = await this.apiService.putCollection(
|
||||
collection.organizationId,
|
||||
collection.id,
|
||||
request,
|
||||
);
|
||||
}
|
||||
|
||||
if (response.assigned) {
|
||||
await this.collectionService.upsert(new CollectionData(response));
|
||||
} else {
|
||||
await this.collectionService.delete(collection.id);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async delete(organizationId: string, collectionId: string): Promise<void> {
|
||||
await this.apiService.deleteCollection(organizationId, collectionId);
|
||||
}
|
||||
|
||||
async bulkAssignAccess(
|
||||
organizationId: string,
|
||||
collectionIds: string[],
|
||||
users: CollectionAccessSelectionView[],
|
||||
groups: CollectionAccessSelectionView[],
|
||||
): Promise<void> {
|
||||
const request = new BulkCollectionAccessRequest();
|
||||
request.collectionIds = collectionIds;
|
||||
request.users = users.map(
|
||||
(u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage),
|
||||
);
|
||||
request.groups = groups.map(
|
||||
(g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords, g.manage),
|
||||
);
|
||||
|
||||
await this.apiService.send(
|
||||
"POST",
|
||||
`/organizations/${organizationId}/collections/bulk-access`,
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
private async decryptMany(
|
||||
organizationId: string,
|
||||
collections: CollectionResponse[] | CollectionAccessDetailsResponse[],
|
||||
): Promise<CollectionAdminView[]> {
|
||||
const orgKey = await this.cryptoService.getOrgKey(organizationId);
|
||||
|
||||
const promises = collections.map(async (c) => {
|
||||
const view = new CollectionAdminView();
|
||||
view.id = c.id;
|
||||
view.name = await this.encryptService.decryptToUtf8(new EncString(c.name), orgKey);
|
||||
view.externalId = c.externalId;
|
||||
view.organizationId = c.organizationId;
|
||||
|
||||
if (isCollectionAccessDetailsResponse(c)) {
|
||||
view.groups = c.groups;
|
||||
view.users = c.users;
|
||||
view.assigned = c.assigned;
|
||||
view.readOnly = c.readOnly;
|
||||
view.hidePasswords = c.hidePasswords;
|
||||
view.manage = c.manage;
|
||||
view.unmanaged = c.unmanaged;
|
||||
}
|
||||
|
||||
return view;
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
private async encrypt(model: CollectionAdminView): Promise<CollectionRequest> {
|
||||
if (model.organizationId == null) {
|
||||
throw new Error("Collection has no organization id.");
|
||||
}
|
||||
const key = await this.cryptoService.getOrgKey(model.organizationId);
|
||||
if (key == null) {
|
||||
throw new Error("No key for this collection's organization.");
|
||||
}
|
||||
const collection = new CollectionRequest();
|
||||
collection.externalId = model.externalId;
|
||||
collection.name = (await this.encryptService.encrypt(model.name, key)).encryptedString;
|
||||
collection.groups = model.groups.map(
|
||||
(group) =>
|
||||
new SelectionReadOnlyRequest(group.id, group.readOnly, group.hidePasswords, group.manage),
|
||||
);
|
||||
collection.users = model.users.map(
|
||||
(user) =>
|
||||
new SelectionReadOnlyRequest(user.id, user.readOnly, user.hidePasswords, user.manage),
|
||||
);
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
function isCollectionAccessDetailsResponse(
|
||||
response: CollectionResponse | CollectionAccessDetailsResponse,
|
||||
): response is CollectionAccessDetailsResponse {
|
||||
const anyResponse = response as any;
|
||||
|
||||
return anyResponse?.groups instanceof Array && anyResponse?.users instanceof Array;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./default-collection-admin.service";
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./organization-user";
|
||||
export * from "./collections";
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
OrganizationUserDetailsResponse,
|
||||
OrganizationUserResetPasswordDetailsResponse,
|
||||
OrganizationUserUserDetailsResponse,
|
||||
OrganizationUserUserMiniResponse,
|
||||
} from "../models/responses";
|
||||
|
||||
/**
|
||||
@@ -44,7 +45,9 @@ export abstract class OrganizationUserApiService {
|
||||
abstract getOrganizationUserGroups(organizationId: string, id: string): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Retrieve a list of all users that belong to the specified organization
|
||||
* Retrieve full details of all users that belong to the specified organization.
|
||||
* This is only accessible to privileged users, if you need a simple listing of basic details, use
|
||||
* {@link getAllMiniUserDetails}.
|
||||
* @param organizationId - Identifier for the organization
|
||||
* @param options - Options for the request
|
||||
*/
|
||||
@@ -56,6 +59,16 @@ export abstract class OrganizationUserApiService {
|
||||
},
|
||||
): Promise<ListResponse<OrganizationUserUserDetailsResponse>>;
|
||||
|
||||
/**
|
||||
* Retrieve a list of all users that belong to the specified organization, with basic information only.
|
||||
* This is suitable for lists of names/emails etc. throughout the app and can be accessed by most users.
|
||||
* @param organizationId - Identifier for the organization
|
||||
* @param options - Options for the request
|
||||
*/
|
||||
abstract getAllMiniUserDetails(
|
||||
organizationId: string,
|
||||
): Promise<ListResponse<OrganizationUserUserMiniResponse>>;
|
||||
|
||||
/**
|
||||
* Retrieve reset password details for the specified organization user
|
||||
* @param organizationId - Identifier for the user's organization
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./organization-user.response";
|
||||
export * from "./organization-user-bulk.response";
|
||||
export * from "./organization-user-bulk-public-key.response";
|
||||
export * from "./organization-user-mini.response";
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
OrganizationUserStatusType,
|
||||
OrganizationUserType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
|
||||
export class OrganizationUserUserMiniResponse extends BaseResponse {
|
||||
id: string;
|
||||
userId: string;
|
||||
email: string;
|
||||
name: string;
|
||||
type: OrganizationUserType;
|
||||
status: OrganizationUserStatusType;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.id = this.getResponseProperty("Id");
|
||||
this.userId = this.getResponseProperty("UserId");
|
||||
this.email = this.getResponseProperty("Email");
|
||||
this.name = this.getResponseProperty("Name");
|
||||
this.type = this.getResponseProperty("Type");
|
||||
this.status = this.getResponseProperty("Status");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { OrganizationUserApiService } from "../abstractions";
|
||||
import {
|
||||
@@ -19,10 +23,14 @@ import {
|
||||
OrganizationUserDetailsResponse,
|
||||
OrganizationUserResetPasswordDetailsResponse,
|
||||
OrganizationUserUserDetailsResponse,
|
||||
OrganizationUserUserMiniResponse,
|
||||
} from "../models/responses";
|
||||
|
||||
export class DefaultOrganizationUserApiService implements OrganizationUserApiService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async getOrganizationUser(
|
||||
organizationId: string,
|
||||
@@ -84,6 +92,27 @@ export class DefaultOrganizationUserApiService implements OrganizationUserApiSer
|
||||
return new ListResponse(r, OrganizationUserUserDetailsResponse);
|
||||
}
|
||||
|
||||
async getAllMiniUserDetails(
|
||||
organizationId: string,
|
||||
): Promise<ListResponse<OrganizationUserUserMiniResponse>> {
|
||||
const apiEnabled = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.Pm3478RefactorOrganizationUserApi),
|
||||
);
|
||||
if (!apiEnabled) {
|
||||
// Keep using the old api until this feature flag is enabled
|
||||
return this.getAllUsers(organizationId);
|
||||
}
|
||||
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
`/organizations/${organizationId}/users/mini-details`,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new ListResponse(r, OrganizationUserUserMiniResponse);
|
||||
}
|
||||
|
||||
async getOrganizationUserResetPasswordDetails(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { ActivatedRoute, NavigationSkipped, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, of } from "rxjs";
|
||||
import { switchMap, take, takeUntil } from "rxjs/operators";
|
||||
|
||||
@@ -123,6 +123,14 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// If the user navigates to /login from /login, reset the validatedEmail flag
|
||||
// This should bring the user back to the login screen with the email field
|
||||
this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => {
|
||||
if (event instanceof NavigationSkipped && event.url === "/login") {
|
||||
this.validatedEmail = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Backup check to handle unknown case where activatedRoute is not available
|
||||
// This shouldn't happen under normal circumstances
|
||||
if (!this.route) {
|
||||
|
||||
@@ -215,7 +215,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
|
||||
// RSA Encrypt user key with organization public key
|
||||
const userKey = await this.cryptoService.getUserKey();
|
||||
const encryptedUserKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
|
||||
const encryptedUserKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.masterPasswordHash = masterPasswordHash;
|
||||
|
||||
@@ -38,6 +38,8 @@ export const authGuard: CanActivateFn = async (
|
||||
if (routerState != null) {
|
||||
messagingService.send("lockedUrl", { url: routerState.url });
|
||||
}
|
||||
// TODO PM-9674: when extension refresh is finished, remove promptBiometric
|
||||
// as it has been integrated into the component as a default feature.
|
||||
return router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } });
|
||||
}
|
||||
|
||||
|
||||
@@ -954,7 +954,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: OrganizationUserApiService,
|
||||
useClass: DefaultOrganizationUserApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
deps: [ApiServiceAbstraction, ConfigService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PasswordResetEnrollmentServiceAbstraction,
|
||||
@@ -963,6 +963,7 @@ const safeProviders: SafeProvider[] = [
|
||||
OrganizationApiServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
EncryptService,
|
||||
OrganizationUserApiService,
|
||||
I18nServiceAbstraction,
|
||||
],
|
||||
@@ -1092,6 +1093,7 @@ const safeProviders: SafeProvider[] = [
|
||||
AccountServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
EncryptService,
|
||||
ApiServiceAbstraction,
|
||||
StateProvider,
|
||||
],
|
||||
@@ -1287,6 +1289,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
KdfConfigServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
|
||||
@@ -308,9 +308,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
this.folders$ = this.folderService.folderViews$;
|
||||
|
||||
if (this.editMode && this.previousCipherId !== this.cipherId) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientViewed, this.cipherId);
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientViewed, [this.cipher]);
|
||||
}
|
||||
this.previousCipherId = this.cipherId;
|
||||
this.reprompt = this.cipher.reprompt !== CipherRepromptType.None;
|
||||
@@ -551,12 +549,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
if (this.editMode && this.showPassword) {
|
||||
document.getElementById("loginPassword")?.focus();
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientToggledPasswordVisible,
|
||||
this.cipherId,
|
||||
);
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientToggledPasswordVisible, [
|
||||
this.cipher,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,23 +561,18 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
if (this.editMode && this.showTotpSeed) {
|
||||
document.getElementById("loginTotp")?.focus();
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientToggledTOTPSeedVisible,
|
||||
this.cipherId,
|
||||
);
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientToggledTOTPSeedVisible, [
|
||||
this.cipher,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleCardNumber() {
|
||||
this.showCardNumber = !this.showCardNumber;
|
||||
if (this.showCardNumber) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(
|
||||
void this.eventCollectionService.collectMany(
|
||||
EventType.Cipher_ClientToggledCardNumberVisible,
|
||||
this.cipherId,
|
||||
[this.cipher],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -591,12 +581,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
this.showCardCode = !this.showCardCode;
|
||||
document.getElementById("cardCode").focus();
|
||||
if (this.editMode && this.showCardCode) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientToggledCardCodeVisible,
|
||||
this.cipherId,
|
||||
);
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientToggledCardCodeVisible, [
|
||||
this.cipher,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -742,17 +729,17 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
if (typeI18nKey === "password") {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, this.cipherId);
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedPassword, [
|
||||
this.cipher,
|
||||
]);
|
||||
} else if (typeI18nKey === "securityCode") {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId);
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedCardCode, [
|
||||
this.cipher,
|
||||
]);
|
||||
} else if (aType === "H_Field") {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId);
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedHiddenField, [
|
||||
this.cipher,
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
'tw-min-h-[calc(100vh-54px)]': clientType === 'desktop',
|
||||
}"
|
||||
>
|
||||
<bit-icon *ngIf="!hideLogo" [icon]="logo" class="tw-w-[128px] [&>*]:tw-align-top"></bit-icon>
|
||||
<a *ngIf="!hideLogo" [routerLink]="['/']" class="tw-w-[128px] [&>*]:tw-align-top">
|
||||
<bit-icon [icon]="logo"></bit-icon>
|
||||
</a>
|
||||
|
||||
<div class="tw-text-center">
|
||||
<div class="tw-mx-auto tw-max-w-28 sm:tw-max-w-32">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
@@ -15,7 +16,7 @@ import { BitwardenLogo, VaultIcon } from "../icons";
|
||||
standalone: true,
|
||||
selector: "auth-anon-layout",
|
||||
templateUrl: "./anon-layout.component.html",
|
||||
imports: [IconModule, CommonModule, TypographyModule, SharedModule],
|
||||
imports: [IconModule, CommonModule, TypographyModule, SharedModule, RouterModule],
|
||||
})
|
||||
export class AnonLayoutComponent implements OnInit, OnChanges {
|
||||
@Input() title: string;
|
||||
|
||||
@@ -49,5 +49,9 @@ export * from "./user-verification/user-verification-dialog.component";
|
||||
export * from "./user-verification/user-verification-dialog.types";
|
||||
export * from "./user-verification/user-verification-form-input.component";
|
||||
|
||||
// vault timeout input
|
||||
// lock
|
||||
export * from "./lock/lock.component";
|
||||
export * from "./lock/lock-component.service";
|
||||
|
||||
// vault timeout
|
||||
export * from "./vault-timeout-input/vault-timeout-input.component";
|
||||
|
||||
48
libs/auth/src/angular/lock/lock-component.service.ts
Normal file
48
libs/auth/src/angular/lock/lock-component.service.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export enum BiometricsDisableReason {
|
||||
NotSupportedOnOperatingSystem = "NotSupportedOnOperatingSystem",
|
||||
EncryptedKeysUnavailable = "BiometricsEncryptedKeysUnavailable",
|
||||
SystemBiometricsUnavailable = "SystemBiometricsUnavailable",
|
||||
}
|
||||
|
||||
// ex: type UnlockOptionValue = "masterPassword" | "pin" | "biometrics"
|
||||
export type UnlockOptionValue = (typeof UnlockOption)[keyof typeof UnlockOption];
|
||||
|
||||
export const UnlockOption = Object.freeze({
|
||||
MasterPassword: "masterPassword",
|
||||
Pin: "pin",
|
||||
Biometrics: "biometrics",
|
||||
}) satisfies { [Prop in keyof UnlockOptions as Capitalize<Prop>]: Prop };
|
||||
|
||||
export type UnlockOptions = {
|
||||
masterPassword: {
|
||||
enabled: boolean;
|
||||
};
|
||||
pin: {
|
||||
enabled: boolean;
|
||||
};
|
||||
biometrics: {
|
||||
enabled: boolean;
|
||||
disableReason: BiometricsDisableReason | null;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The LockComponentService is a service which allows the single libs/auth LockComponent to delegate all
|
||||
* client specific functionality to client specific services implementations of LockComponentService.
|
||||
*/
|
||||
export abstract class LockComponentService {
|
||||
// Extension
|
||||
abstract getBiometricsError(error: any): string | null;
|
||||
abstract getPreviousUrl(): string | null;
|
||||
|
||||
// Desktop only
|
||||
abstract isWindowVisible(): Promise<boolean>;
|
||||
abstract getBiometricsUnlockBtnText(): string;
|
||||
|
||||
// Multi client
|
||||
abstract getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions>;
|
||||
}
|
||||
191
libs/auth/src/angular/lock/lock.component.html
Normal file
191
libs/auth/src/angular/lock/lock.component.html
Normal file
@@ -0,0 +1,191 @@
|
||||
<ng-template #loading>
|
||||
<div class="tw-flex tw-items-center tw-justify-center" *ngIf="loading">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="unlockOptions; else loading">
|
||||
<!-- Biometrics Unlock -->
|
||||
<ng-container
|
||||
*ngIf="unlockOptions.biometrics.enabled && activeUnlockOption === UnlockOption.Biometrics"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
class="tw-mb-3"
|
||||
[disabled]="unlockingViaBiometrics"
|
||||
[loading]="unlockingViaBiometrics"
|
||||
block
|
||||
(click)="unlockViaBiometrics()"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
</button>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-space-y-3">
|
||||
<p class="tw-text-center tw-mb-0">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.pin.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Pin"
|
||||
>
|
||||
{{ "unlockWithPin" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.masterPassword.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.MasterPassword"
|
||||
>
|
||||
{{ "unlockWithMasterPassword" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- PIN Unlock -->
|
||||
<ng-container *ngIf="unlockOptions.pin.enabled && activeUnlockOption === UnlockOption.Pin">
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "pin" | i18n }}</bit-label>
|
||||
<input
|
||||
type="password"
|
||||
formControlName="pin"
|
||||
bitInput
|
||||
appAutofocus
|
||||
name="pin"
|
||||
class="tw-font-mono"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-space-y-3">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" block>
|
||||
{{ "unlock" | i18n }}
|
||||
</button>
|
||||
|
||||
<p class="tw-text-center">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.biometrics.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Biometrics"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.masterPassword.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.MasterPassword"
|
||||
>
|
||||
{{ "unlockWithMasterPassword" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton bitFormButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
<!-- MP Unlock -->
|
||||
<ng-container
|
||||
*ngIf="
|
||||
unlockOptions.masterPassword.enabled && activeUnlockOption === UnlockOption.MasterPassword
|
||||
"
|
||||
>
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
type="password"
|
||||
formControlName="masterPassword"
|
||||
bitInput
|
||||
appAutofocus
|
||||
name="masterPassword"
|
||||
class="tw-font-mono"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
|
||||
<!-- [attr.aria-pressed]="showPassword" -->
|
||||
</bit-form-field>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-space-y-3">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" block>
|
||||
{{ "unlock" | i18n }}
|
||||
</button>
|
||||
|
||||
<p class="tw-text-center">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.biometrics.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Biometrics"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.pin.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Pin"
|
||||
>
|
||||
{{ "unlockWithPin" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton bitFormButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
638
libs/auth/src/angular/lock/lock.component.ts
Normal file
638
libs/auth/src/angular/lock/lock.component.ts
Normal file
@@ -0,0 +1,638 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { BehaviorSubject, firstValueFrom, Subject, switchMap, take, takeUntil } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import {
|
||||
MasterPasswordVerification,
|
||||
MasterPasswordVerificationResponse,
|
||||
} from "@bitwarden/common/auth/types/verification";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
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 { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { PinServiceAbstraction } from "../../common/abstractions";
|
||||
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
|
||||
|
||||
import {
|
||||
UnlockOption,
|
||||
LockComponentService,
|
||||
UnlockOptions,
|
||||
UnlockOptionValue,
|
||||
} from "./lock-component.service";
|
||||
|
||||
const BroadcasterSubscriptionId = "LockComponent";
|
||||
|
||||
const clientTypeToSuccessRouteRecord: Partial<Record<ClientType, string>> = {
|
||||
[ClientType.Web]: "vault",
|
||||
[ClientType.Desktop]: "vault",
|
||||
[ClientType.Browser]: "/tabs/current",
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "bit-lock",
|
||||
templateUrl: "lock.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
ReactiveFormsModule,
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
IconButtonModule,
|
||||
],
|
||||
})
|
||||
export class LockV2Component implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
activeAccount: { id: UserId | undefined } & AccountInfo;
|
||||
|
||||
clientType: ClientType;
|
||||
ClientType = ClientType;
|
||||
|
||||
unlockOptions: UnlockOptions = null;
|
||||
|
||||
UnlockOption = UnlockOption;
|
||||
|
||||
private _activeUnlockOptionBSubject: BehaviorSubject<UnlockOptionValue> =
|
||||
new BehaviorSubject<UnlockOptionValue>(null);
|
||||
|
||||
activeUnlockOption$ = this._activeUnlockOptionBSubject.asObservable();
|
||||
|
||||
set activeUnlockOption(value: UnlockOptionValue) {
|
||||
this._activeUnlockOptionBSubject.next(value);
|
||||
}
|
||||
|
||||
get activeUnlockOption(): UnlockOptionValue {
|
||||
return this._activeUnlockOptionBSubject.value;
|
||||
}
|
||||
|
||||
private invalidPinAttempts = 0;
|
||||
|
||||
biometricUnlockBtnText: string;
|
||||
|
||||
// masterPassword = "";
|
||||
showPassword = false;
|
||||
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
|
||||
|
||||
forcePasswordResetRoute = "update-temp-password";
|
||||
|
||||
formGroup: FormGroup;
|
||||
|
||||
// Desktop properties:
|
||||
private deferFocus: boolean = null;
|
||||
private biometricAsked = false;
|
||||
|
||||
// Browser extension properties:
|
||||
private isInitialLockScreen = (window as any).previousPopupUrl == null;
|
||||
|
||||
defaultUnlockOptionSetForUser = false;
|
||||
|
||||
unlockingViaBiometrics = false;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private pinService: PinServiceAbstraction,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private cryptoService: CryptoService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private router: Router,
|
||||
private dialogService: DialogService,
|
||||
private messagingService: MessagingService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private ngZone: NgZone,
|
||||
private i18nService: I18nService,
|
||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
private syncService: SyncService,
|
||||
private policyService: InternalPolicyService,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
private formBuilder: FormBuilder,
|
||||
private toastService: ToastService,
|
||||
|
||||
private lockComponentService: LockComponentService,
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
|
||||
// desktop deps
|
||||
private broadcasterService: BroadcasterService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.listenForActiveUnlockOptionChanges();
|
||||
|
||||
// Listen for active account changes
|
||||
this.listenForActiveAccountChanges();
|
||||
|
||||
// Identify client
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
|
||||
if (this.clientType === "desktop") {
|
||||
await this.desktopOnInit();
|
||||
}
|
||||
}
|
||||
|
||||
// Base component methods
|
||||
private listenForActiveUnlockOptionChanges() {
|
||||
this.activeUnlockOption$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((activeUnlockOption: UnlockOptionValue) => {
|
||||
if (activeUnlockOption === UnlockOption.Pin) {
|
||||
this.buildPinForm();
|
||||
} else if (activeUnlockOption === UnlockOption.MasterPassword) {
|
||||
this.buildMasterPasswordForm();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private buildMasterPasswordForm() {
|
||||
this.formGroup = this.formBuilder.group(
|
||||
{
|
||||
masterPassword: ["", [Validators.required]],
|
||||
},
|
||||
{ updateOn: "submit" },
|
||||
);
|
||||
}
|
||||
|
||||
private buildPinForm() {
|
||||
this.formGroup = this.formBuilder.group(
|
||||
{
|
||||
pin: ["", [Validators.required]],
|
||||
},
|
||||
{ updateOn: "submit" },
|
||||
);
|
||||
}
|
||||
|
||||
private listenForActiveAccountChanges() {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
switchMap((account) => {
|
||||
return this.handleActiveAccountChange(account);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private async handleActiveAccountChange(activeAccount: { id: UserId | undefined } & AccountInfo) {
|
||||
this.activeAccount = activeAccount;
|
||||
|
||||
this.resetDataOnActiveAccountChange();
|
||||
|
||||
this.setEmailAsPageSubtitle(activeAccount.email);
|
||||
|
||||
this.unlockOptions = await firstValueFrom(
|
||||
this.lockComponentService.getAvailableUnlockOptions$(activeAccount.id),
|
||||
);
|
||||
|
||||
this.setDefaultActiveUnlockOption(this.unlockOptions);
|
||||
|
||||
if (this.unlockOptions.biometrics.enabled) {
|
||||
await this.handleBiometricsUnlockEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
private resetDataOnActiveAccountChange() {
|
||||
this.defaultUnlockOptionSetForUser = false;
|
||||
this.unlockOptions = null;
|
||||
this.activeUnlockOption = null;
|
||||
this.formGroup = null; // new form group will be created based on new active unlock option
|
||||
|
||||
// Desktop properties:
|
||||
this.biometricAsked = false;
|
||||
}
|
||||
|
||||
private setEmailAsPageSubtitle(email: string) {
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageSubtitle: {
|
||||
subtitle: email,
|
||||
translate: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private setDefaultActiveUnlockOption(unlockOptions: UnlockOptions) {
|
||||
// Priorities should be Biometrics > Pin > Master Password for speed
|
||||
if (unlockOptions.biometrics.enabled) {
|
||||
this.activeUnlockOption = UnlockOption.Biometrics;
|
||||
} else if (unlockOptions.pin.enabled) {
|
||||
this.activeUnlockOption = UnlockOption.Pin;
|
||||
} else if (unlockOptions.masterPassword.enabled) {
|
||||
this.activeUnlockOption = UnlockOption.MasterPassword;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleBiometricsUnlockEnabled() {
|
||||
this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText();
|
||||
|
||||
const autoPromptBiometrics = await firstValueFrom(
|
||||
this.biometricStateService.promptAutomatically$,
|
||||
);
|
||||
|
||||
// TODO: PM-12546 - we need to make our biometric autoprompt experience consistent between the
|
||||
// desktop and extension.
|
||||
if (this.clientType === "desktop") {
|
||||
if (autoPromptBiometrics) {
|
||||
await this.desktopAutoPromptBiometrics();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.clientType === "browser") {
|
||||
if (
|
||||
this.unlockOptions.biometrics.enabled &&
|
||||
autoPromptBiometrics &&
|
||||
this.isInitialLockScreen // only autoprompt biometrics on initial lock screen
|
||||
) {
|
||||
await this.unlockViaBiometrics();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this submit method is only used for unlock methods that require a form and user input.
|
||||
// For biometrics unlock, the method is called directly.
|
||||
submit = async (): Promise<void> => {
|
||||
if (this.activeUnlockOption === UnlockOption.Pin) {
|
||||
return await this.unlockViaPin();
|
||||
}
|
||||
|
||||
await this.unlockViaMasterPassword();
|
||||
};
|
||||
|
||||
async logOut() {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "logOut" },
|
||||
content: { key: "logOutConfirmation" },
|
||||
acceptButtonText: { key: "logOut" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.messagingService.send("logout", { userId: this.activeAccount.id });
|
||||
}
|
||||
}
|
||||
|
||||
async unlockViaBiometrics(): Promise<void> {
|
||||
this.unlockingViaBiometrics = true;
|
||||
|
||||
if (!this.unlockOptions.biometrics.enabled) {
|
||||
this.unlockingViaBiometrics = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.biometricStateService.setUserPromptCancelled();
|
||||
const userKey = await this.cryptoService.getUserKeyFromStorage(
|
||||
KeySuffixOptions.Biometric,
|
||||
this.activeAccount.id,
|
||||
);
|
||||
|
||||
// If user cancels biometric prompt, userKey is undefined.
|
||||
if (userKey) {
|
||||
await this.setUserKeyAndContinue(userKey, false);
|
||||
}
|
||||
|
||||
this.unlockingViaBiometrics = false;
|
||||
} catch (e) {
|
||||
// Cancelling is a valid action.
|
||||
if (e?.message === "canceled") {
|
||||
this.unlockingViaBiometrics = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let biometricTranslatedErrorDesc;
|
||||
|
||||
if (this.clientType === "browser") {
|
||||
const biometricErrorDescTranslationKey = this.lockComponentService.getBiometricsError(e);
|
||||
|
||||
if (biometricErrorDescTranslationKey) {
|
||||
biometricTranslatedErrorDesc = this.i18nService.t(biometricErrorDescTranslationKey);
|
||||
}
|
||||
}
|
||||
|
||||
// if no translation key found, show generic error message
|
||||
if (!biometricTranslatedErrorDesc) {
|
||||
biometricTranslatedErrorDesc = this.i18nService.t("unexpectedError");
|
||||
}
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "error" },
|
||||
content: biometricTranslatedErrorDesc,
|
||||
acceptButtonText: { key: "tryAgain" },
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
// try again
|
||||
await this.unlockViaBiometrics();
|
||||
}
|
||||
|
||||
this.unlockingViaBiometrics = false;
|
||||
}
|
||||
}
|
||||
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword;
|
||||
const input = document.getElementById(
|
||||
this.unlockOptions.pin.enabled ? "pin" : "masterPassword",
|
||||
);
|
||||
if (this.ngZone.isStable) {
|
||||
input.focus();
|
||||
} else {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus());
|
||||
}
|
||||
}
|
||||
|
||||
private validatePin(): boolean {
|
||||
if (this.formGroup.invalid) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("pinRequired"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async unlockViaPin() {
|
||||
if (!this.validatePin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pin = this.formGroup.controls.pin.value;
|
||||
|
||||
const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5;
|
||||
|
||||
try {
|
||||
const userKey = await this.pinService.decryptUserKeyWithPin(pin, this.activeAccount.id);
|
||||
|
||||
if (userKey) {
|
||||
await this.setUserKeyAndContinue(userKey);
|
||||
return; // successfully unlocked
|
||||
}
|
||||
|
||||
// Failure state: invalid PIN or failed decryption
|
||||
this.invalidPinAttempts++;
|
||||
|
||||
// Log user out if they have entered an invalid PIN too many times
|
||||
if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"),
|
||||
});
|
||||
this.messagingService.send("logout");
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("invalidPin"),
|
||||
});
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private validateMasterPassword(): boolean {
|
||||
if (this.formGroup.invalid) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordRequired"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async unlockViaMasterPassword() {
|
||||
if (!this.validateMasterPassword()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const masterPassword = this.formGroup.controls.masterPassword.value;
|
||||
|
||||
const verification = {
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: masterPassword,
|
||||
} as MasterPasswordVerification;
|
||||
|
||||
let passwordValid = false;
|
||||
let masterPasswordVerificationResponse: MasterPasswordVerificationResponse;
|
||||
try {
|
||||
masterPasswordVerificationResponse =
|
||||
await this.userVerificationService.verifyUserByMasterPassword(
|
||||
verification,
|
||||
this.activeAccount.id,
|
||||
this.activeAccount.email,
|
||||
);
|
||||
|
||||
this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse(
|
||||
masterPasswordVerificationResponse.policyOptions,
|
||||
);
|
||||
passwordValid = true;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (!passwordValid) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("invalidMasterPassword"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
masterPasswordVerificationResponse.masterKey,
|
||||
);
|
||||
await this.setUserKeyAndContinue(userKey, true);
|
||||
}
|
||||
|
||||
private async setUserKeyAndContinue(key: UserKey, evaluatePasswordAfterUnlock = false) {
|
||||
await this.cryptoService.setUserKey(key, this.activeAccount.id);
|
||||
|
||||
// Now that we have a decrypted user key in memory, we can check if we
|
||||
// need to establish trust on the current device
|
||||
await this.deviceTrustService.trustDeviceIfRequired(this.activeAccount.id);
|
||||
|
||||
await this.doContinue(evaluatePasswordAfterUnlock);
|
||||
}
|
||||
|
||||
private async doContinue(evaluatePasswordAfterUnlock: boolean) {
|
||||
await this.biometricStateService.resetUserPromptCancelled();
|
||||
this.messagingService.send("unlocked");
|
||||
|
||||
if (evaluatePasswordAfterUnlock) {
|
||||
try {
|
||||
// If we do not have any saved policies, attempt to load them from the service
|
||||
if (this.enforcedMasterPasswordOptions == undefined) {
|
||||
this.enforcedMasterPasswordOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.requirePasswordChange()) {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
userId,
|
||||
);
|
||||
await this.router.navigate([this.forcePasswordResetRoute]);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Do not prevent unlock if there is an error evaluating policies
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Vault can be de-synced since notifications get ignored while locked. Need to check whether sync is required using the sync service.
|
||||
await this.syncService.fullSync(false);
|
||||
|
||||
if (this.clientType === "browser") {
|
||||
const previousUrl = this.lockComponentService.getPreviousUrl();
|
||||
if (previousUrl) {
|
||||
await this.router.navigateByUrl(previousUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// determine success route based on client type
|
||||
const successRoute = clientTypeToSuccessRouteRecord[this.clientType];
|
||||
await this.router.navigate([successRoute]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the master password meets the enforced policy requirements
|
||||
* If not, returns false
|
||||
*/
|
||||
private requirePasswordChange(): boolean {
|
||||
if (
|
||||
this.enforcedMasterPasswordOptions == undefined ||
|
||||
!this.enforcedMasterPasswordOptions.enforceOnLogin
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const masterPassword = this.formGroup.controls.masterPassword.value;
|
||||
|
||||
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
|
||||
masterPassword,
|
||||
this.activeAccount.email,
|
||||
)?.score;
|
||||
|
||||
return !this.policyService.evaluateMasterPassword(
|
||||
passwordStrength,
|
||||
masterPassword,
|
||||
this.enforcedMasterPasswordOptions,
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Desktop methods:
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
async desktopOnInit() {
|
||||
// TODO: move this into a WindowService and subscribe to messages via MessageListener service.
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
switch (message.command) {
|
||||
case "windowHidden":
|
||||
this.onWindowHidden();
|
||||
break;
|
||||
case "windowIsFocused":
|
||||
if (this.deferFocus === null) {
|
||||
this.deferFocus = !message.windowIsFocused;
|
||||
if (!this.deferFocus) {
|
||||
this.focusInput();
|
||||
}
|
||||
} else if (this.deferFocus && message.windowIsFocused) {
|
||||
this.focusInput();
|
||||
this.deferFocus = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
this.messagingService.send("getWindowIsFocused");
|
||||
}
|
||||
|
||||
private async desktopAutoPromptBiometrics() {
|
||||
if (!this.unlockOptions?.biometrics?.enabled || this.biometricAsked) {
|
||||
return;
|
||||
}
|
||||
|
||||
// prevent the biometric prompt from showing if the user has already cancelled it
|
||||
if (await firstValueFrom(this.biometricStateService.promptCancelled$)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const windowVisible = await this.lockComponentService.isWindowVisible();
|
||||
|
||||
if (windowVisible) {
|
||||
this.biometricAsked = true;
|
||||
await this.unlockViaBiometrics();
|
||||
}
|
||||
}
|
||||
|
||||
onWindowHidden() {
|
||||
this.showPassword = false;
|
||||
}
|
||||
|
||||
private focusInput() {
|
||||
if (this.unlockOptions) {
|
||||
document.getElementById(this.unlockOptions.pin.enabled ? "pin" : "masterPassword")?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
||||
if (this.clientType === "desktop") {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-con
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
@@ -33,6 +34,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
|
||||
@@ -43,6 +45,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
cryptoService = mock<CryptoService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
i18nService = mock<I18nService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
@@ -53,6 +56,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
sut = new DefaultSetPasswordJitService(
|
||||
apiService,
|
||||
cryptoService,
|
||||
encryptService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
masterPasswordService,
|
||||
@@ -168,7 +172,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
}
|
||||
|
||||
cryptoService.userKey$.mockReturnValue(of(userKey));
|
||||
cryptoService.rsaEncrypt.mockResolvedValue(userKeyEncString);
|
||||
encryptService.rsaEncrypt.mockResolvedValue(userKeyEncString);
|
||||
|
||||
organizationUserApiService.putOrganizationUserResetPasswordEnrollment.mockResolvedValue(
|
||||
undefined,
|
||||
@@ -210,7 +214,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
// Assert
|
||||
expect(apiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(organizationApiService.getKeys).toHaveBeenCalledWith(orgId);
|
||||
expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(userKey.key, orgPublicKey);
|
||||
expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(userKey.key, orgPublicKey);
|
||||
expect(
|
||||
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
|
||||
).toHaveBeenCalled();
|
||||
|
||||
@@ -14,6 +14,7 @@ import { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
@@ -29,6 +30,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected cryptoService: CryptoService,
|
||||
protected encryptService: EncryptService,
|
||||
protected i18nService: I18nService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
@@ -157,7 +159,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
throw new Error("userKey not found. Could not handle reset password auto enroll.");
|
||||
}
|
||||
|
||||
const encryptedUserKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
|
||||
const encryptedUserKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.masterPasswordHash = masterKeyHash;
|
||||
|
||||
@@ -226,7 +226,7 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
const mockUserKey = new SymmetricCryptoKey(mockUserKeyArray) as UserKey;
|
||||
|
||||
encryptService.decryptToBytes.mockResolvedValue(mockPrfPrivateKey);
|
||||
cryptoService.rsaDecrypt.mockResolvedValue(mockUserKeyArray);
|
||||
encryptService.rsaDecrypt.mockResolvedValue(mockUserKeyArray);
|
||||
|
||||
// Act
|
||||
await webAuthnLoginStrategy.logIn(webAuthnCredentials);
|
||||
@@ -244,9 +244,9 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedPrivateKey,
|
||||
webAuthnCredentials.prfKey,
|
||||
);
|
||||
expect(cryptoService.rsaDecrypt).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoService.rsaDecrypt).toHaveBeenCalledWith(
|
||||
idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey.encryptedString,
|
||||
expect(encryptService.rsaDecrypt).toHaveBeenCalledTimes(1);
|
||||
expect(encryptService.rsaDecrypt).toHaveBeenCalledWith(
|
||||
idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey,
|
||||
mockPrfPrivateKey,
|
||||
);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId);
|
||||
@@ -273,7 +273,7 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
|
||||
// Assert
|
||||
expect(encryptService.decryptToBytes).not.toHaveBeenCalled();
|
||||
expect(cryptoService.rsaDecrypt).not.toHaveBeenCalled();
|
||||
expect(encryptService.rsaDecrypt).not.toHaveBeenCalled();
|
||||
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -325,7 +325,7 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
|
||||
|
||||
cryptoService.rsaDecrypt.mockResolvedValue(null);
|
||||
encryptService.rsaDecrypt.mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
await webAuthnLoginStrategy.logIn(webAuthnCredentials);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Jsonify } from "type-fest";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
@@ -86,8 +87,8 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||
);
|
||||
|
||||
// decrypt user key with private key
|
||||
const userKey = await this.cryptoService.rsaDecrypt(
|
||||
webAuthnPrfOption.encryptedUserKey.encryptedString,
|
||||
const userKey = await this.encryptService.rsaDecrypt(
|
||||
new EncString(webAuthnPrfOption.encryptedUserKey.encryptedString),
|
||||
privateKey,
|
||||
);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/maste
|
||||
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
@@ -24,6 +25,7 @@ describe("AuthRequestService", () => {
|
||||
let masterPasswordService: FakeMasterPasswordService;
|
||||
const appIdService = mock<AppIdService>();
|
||||
const cryptoService = mock<CryptoService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
const apiService = mock<ApiService>();
|
||||
|
||||
let mockPrivateKey: Uint8Array;
|
||||
@@ -40,6 +42,7 @@ describe("AuthRequestService", () => {
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
cryptoService,
|
||||
encryptService,
|
||||
apiService,
|
||||
stateProvider,
|
||||
);
|
||||
@@ -82,7 +85,7 @@ describe("AuthRequestService", () => {
|
||||
|
||||
describe("approveOrDenyAuthRequest", () => {
|
||||
beforeEach(() => {
|
||||
cryptoService.rsaEncrypt.mockResolvedValue({
|
||||
encryptService.rsaEncrypt.mockResolvedValue({
|
||||
encryptedString: "ENCRYPTED_STRING",
|
||||
} as EncString);
|
||||
appIdService.getAppId.mockResolvedValue("APP_ID");
|
||||
@@ -108,7 +111,7 @@ describe("AuthRequestService", () => {
|
||||
new AuthRequestResponse({ id: "123", publicKey: "KEY" }),
|
||||
);
|
||||
|
||||
expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything());
|
||||
expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything());
|
||||
});
|
||||
|
||||
it("should use the user key if the master key and hash do not exist", async () => {
|
||||
@@ -119,7 +122,7 @@ describe("AuthRequestService", () => {
|
||||
new AuthRequestResponse({ id: "123", publicKey: "KEY" }),
|
||||
);
|
||||
|
||||
expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything());
|
||||
expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything());
|
||||
});
|
||||
});
|
||||
describe("setUserKeyAfterDecryptingSharedUserKey", () => {
|
||||
@@ -211,7 +214,7 @@ describe("AuthRequestService", () => {
|
||||
const mockDecryptedUserKeyBytes = new Uint8Array(64);
|
||||
const mockDecryptedUserKey = new SymmetricCryptoKey(mockDecryptedUserKeyBytes) as UserKey;
|
||||
|
||||
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedUserKeyBytes);
|
||||
encryptService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedUserKeyBytes);
|
||||
|
||||
// Act
|
||||
const result = await sut.decryptPubKeyEncryptedUserKey(
|
||||
@@ -220,7 +223,10 @@ describe("AuthRequestService", () => {
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(cryptoService.rsaDecrypt).toBeCalledWith(mockPubKeyEncryptedUserKey, mockPrivateKey);
|
||||
expect(encryptService.rsaDecrypt).toBeCalledWith(
|
||||
new EncString(mockPubKeyEncryptedUserKey),
|
||||
mockPrivateKey,
|
||||
);
|
||||
expect(result).toEqual(mockDecryptedUserKey);
|
||||
});
|
||||
});
|
||||
@@ -238,7 +244,7 @@ describe("AuthRequestService", () => {
|
||||
const mockDecryptedMasterKeyHashBytes = new Uint8Array(64);
|
||||
const mockDecryptedMasterKeyHash = Utils.fromBufferToUtf8(mockDecryptedMasterKeyHashBytes);
|
||||
|
||||
cryptoService.rsaDecrypt
|
||||
encryptService.rsaDecrypt
|
||||
.mockResolvedValueOnce(mockDecryptedMasterKeyBytes)
|
||||
.mockResolvedValueOnce(mockDecryptedMasterKeyHashBytes);
|
||||
|
||||
@@ -250,14 +256,14 @@ describe("AuthRequestService", () => {
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
|
||||
expect(encryptService.rsaDecrypt).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
mockPubKeyEncryptedMasterKey,
|
||||
new EncString(mockPubKeyEncryptedMasterKey),
|
||||
mockPrivateKey,
|
||||
);
|
||||
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
|
||||
expect(encryptService.rsaDecrypt).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
mockPubKeyEncryptedMasterKeyHash,
|
||||
new EncString(mockPubKeyEncryptedMasterKeyHash),
|
||||
mockPrivateKey,
|
||||
);
|
||||
expect(result.masterKey).toEqual(mockDecryptedMasterKey);
|
||||
|
||||
@@ -10,7 +10,9 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth
|
||||
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
AUTH_REQUEST_DISK_LOCAL,
|
||||
@@ -44,6 +46,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||
private accountService: AccountService,
|
||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
private cryptoService: CryptoService,
|
||||
private encryptService: EncryptService,
|
||||
private apiService: ApiService,
|
||||
private stateProvider: StateProvider,
|
||||
) {
|
||||
@@ -102,7 +105,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||
if (masterKey && masterKeyHash) {
|
||||
// Only encrypt the master password hash if masterKey exists as
|
||||
// we won't have a masterKeyHash without a masterKey
|
||||
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
|
||||
encryptedMasterKeyHash = await this.encryptService.rsaEncrypt(
|
||||
Utils.fromUtf8ToArray(masterKeyHash),
|
||||
pubKey,
|
||||
);
|
||||
@@ -112,7 +115,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||
keyToEncrypt = userKey.key;
|
||||
}
|
||||
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey);
|
||||
const encryptedKey = await this.encryptService.rsaEncrypt(keyToEncrypt, pubKey);
|
||||
|
||||
const response = new PasswordlessAuthRequest(
|
||||
encryptedKey.encryptedString,
|
||||
@@ -161,8 +164,8 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||
pubKeyEncryptedUserKey: string,
|
||||
privateKey: Uint8Array,
|
||||
): Promise<UserKey> {
|
||||
const decryptedUserKeyBytes = await this.cryptoService.rsaDecrypt(
|
||||
pubKeyEncryptedUserKey,
|
||||
const decryptedUserKeyBytes = await this.encryptService.rsaDecrypt(
|
||||
new EncString(pubKeyEncryptedUserKey),
|
||||
privateKey,
|
||||
);
|
||||
|
||||
@@ -174,13 +177,13 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||
pubKeyEncryptedMasterKeyHash: string,
|
||||
privateKey: Uint8Array,
|
||||
): Promise<{ masterKey: MasterKey; masterKeyHash: string }> {
|
||||
const decryptedMasterKeyArrayBuffer = await this.cryptoService.rsaDecrypt(
|
||||
pubKeyEncryptedMasterKey,
|
||||
const decryptedMasterKeyArrayBuffer = await this.encryptService.rsaDecrypt(
|
||||
new EncString(pubKeyEncryptedMasterKey),
|
||||
privateKey,
|
||||
);
|
||||
|
||||
const decryptedMasterKeyHashArrayBuffer = await this.cryptoService.rsaDecrypt(
|
||||
pubKeyEncryptedMasterKeyHash,
|
||||
const decryptedMasterKeyHashArrayBuffer = await this.encryptService.rsaDecrypt(
|
||||
new EncString(pubKeyEncryptedMasterKeyHash),
|
||||
privateKey,
|
||||
);
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
deviceKeyEncryptedDevicePrivateKey,
|
||||
] = await Promise.all([
|
||||
// Encrypt user key with the DevicePublicKey
|
||||
this.cryptoService.rsaEncrypt(userKey.key, devicePublicKey),
|
||||
this.encryptService.rsaEncrypt(userKey.key, devicePublicKey),
|
||||
|
||||
// Encrypt devicePublicKey with user key
|
||||
this.encryptService.encrypt(devicePublicKey, userKey),
|
||||
@@ -206,7 +206,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
);
|
||||
|
||||
// Encrypt the brand new user key with the now-decrypted public key for the device
|
||||
const encryptedNewUserKey = await this.cryptoService.rsaEncrypt(
|
||||
const encryptedNewUserKey = await this.encryptService.rsaEncrypt(
|
||||
newUserKey.key,
|
||||
decryptedDevicePublicKey,
|
||||
);
|
||||
@@ -317,8 +317,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
);
|
||||
|
||||
// Attempt to decrypt encryptedUserDataKey with devicePrivateKey
|
||||
const userKey = await this.cryptoService.rsaDecrypt(
|
||||
encryptedUserKey.encryptedString,
|
||||
const userKey = await this.encryptService.rsaDecrypt(
|
||||
new EncString(encryptedUserKey.encryptedString),
|
||||
devicePrivateKey,
|
||||
);
|
||||
|
||||
|
||||
@@ -372,7 +372,7 @@ describe("deviceTrustService", () => {
|
||||
.mockResolvedValue(mockUserKey);
|
||||
|
||||
cryptoSvcRsaEncryptSpy = jest
|
||||
.spyOn(cryptoService, "rsaEncrypt")
|
||||
.spyOn(encryptService, "rsaEncrypt")
|
||||
.mockResolvedValue(mockDevicePublicKeyEncryptedUserKey);
|
||||
|
||||
encryptServiceEncryptSpy = jest
|
||||
@@ -577,7 +577,7 @@ describe("deviceTrustService", () => {
|
||||
.spyOn(encryptService, "decryptToBytes")
|
||||
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
|
||||
const rsaDecryptSpy = jest
|
||||
.spyOn(cryptoService, "rsaDecrypt")
|
||||
.spyOn(encryptService, "rsaDecrypt")
|
||||
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
|
||||
|
||||
const result = await deviceTrustService.decryptUserKeyWithDeviceKey(
|
||||
@@ -696,7 +696,7 @@ describe("deviceTrustService", () => {
|
||||
});
|
||||
|
||||
// Mock the encryption of the new user key with the decrypted public key
|
||||
cryptoService.rsaEncrypt.mockImplementationOnce((data, publicKey) => {
|
||||
encryptService.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';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
|
||||
import { UserId } from "../../../../common/src/types/guid";
|
||||
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
@@ -18,6 +19,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
|
||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let service: PasswordResetEnrollmentServiceImplementation;
|
||||
@@ -27,12 +29,14 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
|
||||
accountService = mock<AccountService>();
|
||||
accountService.activeAccount$ = activeAccountSubject;
|
||||
cryptoService = mock<CryptoService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
organizationUserApiService = mock<OrganizationUserApiService>();
|
||||
i18nService = mock<I18nService>();
|
||||
service = new PasswordResetEnrollmentServiceImplementation(
|
||||
organizationApiService,
|
||||
accountService,
|
||||
cryptoService,
|
||||
encryptService,
|
||||
organizationUserApiService,
|
||||
i18nService,
|
||||
);
|
||||
@@ -96,7 +100,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
|
||||
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
|
||||
|
||||
cryptoService.getUserKey.mockResolvedValue({ key: "key" } as any);
|
||||
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
|
||||
encryptService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
|
||||
|
||||
await service.enroll("orgId");
|
||||
|
||||
@@ -118,7 +122,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
|
||||
};
|
||||
const encryptedKey = { encryptedString: "encryptedString" };
|
||||
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
|
||||
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
|
||||
encryptService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
|
||||
|
||||
await service.enroll("orgId", "userId", { key: "key" } as any);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserResetPasswordEnrollmentRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
|
||||
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
@@ -20,6 +21,7 @@ export class PasswordResetEnrollmentServiceImplementation
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected accountService: AccountService,
|
||||
protected cryptoService: CryptoService,
|
||||
protected encryptService: EncryptService,
|
||||
protected organizationUserApiService: OrganizationUserApiService,
|
||||
protected i18nService: I18nService,
|
||||
) {}
|
||||
@@ -47,7 +49,7 @@ export class PasswordResetEnrollmentServiceImplementation
|
||||
userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
|
||||
userKey = userKey ?? (await this.cryptoService.getUserKey(userId));
|
||||
// RSA Encrypt user's userKey.key with organization public key
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, orgPublicKey);
|
||||
const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, orgPublicKey);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
|
||||
|
||||
@@ -9,7 +9,6 @@ export enum FeatureFlag {
|
||||
GeneratorToolsModernization = "generator-tools-modernization",
|
||||
EnableConsolidatedBilling = "enable-consolidated-billing",
|
||||
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
|
||||
EnableDeleteProvider = "AC-1218-delete-provider",
|
||||
ExtensionRefresh = "extension-refresh",
|
||||
PersistPopupView = "persist-popup-view",
|
||||
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
|
||||
@@ -34,6 +33,7 @@ export enum FeatureFlag {
|
||||
AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader",
|
||||
Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
@@ -53,7 +53,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.GeneratorToolsModernization]: FALSE,
|
||||
[FeatureFlag.EnableConsolidatedBilling]: FALSE,
|
||||
[FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE,
|
||||
[FeatureFlag.EnableDeleteProvider]: FALSE,
|
||||
[FeatureFlag.ExtensionRefresh]: FALSE,
|
||||
[FeatureFlag.PersistPopupView]: FALSE,
|
||||
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
|
||||
@@ -78,6 +77,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE,
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
[FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE,
|
||||
[FeatureFlag.Pm3478RefactorOrganizationUserApi]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
@@ -329,22 +329,6 @@ export abstract class CryptoService {
|
||||
* @param userId The user's Id
|
||||
*/
|
||||
abstract clearKeys(userId?: string): Promise<any>;
|
||||
/**
|
||||
* RSA encrypts a value.
|
||||
* @param data The data to encrypt
|
||||
* @param publicKey The public key to use for encryption, if not provided, the user's public key will be used
|
||||
* @returns The encrypted data
|
||||
* @throws If the given publicKey is a null-ish value.
|
||||
*/
|
||||
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
|
||||
/**
|
||||
* Decrypts a value using RSA.
|
||||
* @param encValue The encrypted value to decrypt
|
||||
* @param privateKey The private key to use for decryption
|
||||
* @returns The decrypted value
|
||||
* @throws If the given privateKey is a null-ish value.
|
||||
*/
|
||||
abstract rsaDecrypt(encValue: string, privateKey: Uint8Array): Promise<Uint8Array>;
|
||||
abstract randomNumber(min: number, max: number): Promise<number>;
|
||||
/**
|
||||
* Generates a new cipher key
|
||||
|
||||
@@ -45,7 +45,7 @@ import { KeyGenerationService } from "../abstractions/key-generation.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { KeySuffixOptions, HashPurpose, EncryptionType } from "../enums";
|
||||
import { KeySuffixOptions, HashPurpose } from "../enums";
|
||||
import { convertValues } from "../misc/convert-values";
|
||||
import { EFFLongWordList } from "../misc/wordlist";
|
||||
import { EncString, EncryptedString } from "../models/domain/enc-string";
|
||||
@@ -441,7 +441,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
const shareKey = await this.keyGenerationService.createKey(512);
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
const publicKey = await firstValueFrom(this.userPublicKey$(userId));
|
||||
const encShareKey = await this.rsaEncrypt(shareKey.key, publicKey);
|
||||
const encShareKey = await this.encryptService.rsaEncrypt(shareKey.key, publicKey);
|
||||
return [encShareKey, shareKey as T];
|
||||
}
|
||||
|
||||
@@ -550,68 +550,6 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId);
|
||||
}
|
||||
|
||||
async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString> {
|
||||
if (publicKey == null) {
|
||||
throw new Error("'publicKey' is a required parameter and must be non-null");
|
||||
}
|
||||
|
||||
const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1");
|
||||
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes));
|
||||
}
|
||||
|
||||
async rsaDecrypt(encValue: string, privateKey: Uint8Array): Promise<Uint8Array> {
|
||||
if (privateKey == null) {
|
||||
throw new Error("'privateKey' is a required parameter and must be non-null");
|
||||
}
|
||||
|
||||
const headerPieces = encValue.split(".");
|
||||
let encType: EncryptionType = null;
|
||||
let encPieces: string[];
|
||||
|
||||
if (headerPieces.length === 1) {
|
||||
encType = EncryptionType.Rsa2048_OaepSha256_B64;
|
||||
encPieces = [headerPieces[0]];
|
||||
} else if (headerPieces.length === 2) {
|
||||
try {
|
||||
encType = parseInt(headerPieces[0], null);
|
||||
encPieces = headerPieces[1].split("|");
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
switch (encType) {
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: // HmacSha256 types are deprecated
|
||||
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
break;
|
||||
default:
|
||||
throw new Error("encType unavailable.");
|
||||
}
|
||||
|
||||
if (encPieces == null || encPieces.length <= 0) {
|
||||
throw new Error("encPieces unavailable.");
|
||||
}
|
||||
|
||||
const data = Utils.fromB64ToArray(encPieces[0]);
|
||||
|
||||
let alg: "sha1" | "sha256" = "sha1";
|
||||
switch (encType) {
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
|
||||
alg = "sha256";
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
break;
|
||||
default:
|
||||
throw new Error("encType unavailable.");
|
||||
}
|
||||
|
||||
return this.cryptoFunctionService.rsaDecrypt(data, privateKey, alg);
|
||||
}
|
||||
|
||||
// EFForg/OpenWireless
|
||||
// ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js
|
||||
async randomNumber(min: number, max: number): Promise<number> {
|
||||
|
||||
@@ -36,5 +36,5 @@ export abstract class SendApiService {
|
||||
renewSendFileUploadUrl: (sendId: string, fileId: string) => Promise<SendFileUploadDataResponse>;
|
||||
removePassword: (id: string) => Promise<any>;
|
||||
delete: (id: string) => Promise<any>;
|
||||
save: (sendData: [Send, EncArrayBuffer]) => Promise<any>;
|
||||
save: (sendData: [Send, EncArrayBuffer]) => Promise<Send>;
|
||||
}
|
||||
|
||||
@@ -135,11 +135,12 @@ export class SendApiService implements SendApiServiceAbstraction {
|
||||
return this.apiService.send("DELETE", "/sends/" + id, null, true, false);
|
||||
}
|
||||
|
||||
async save(sendData: [Send, EncArrayBuffer]): Promise<any> {
|
||||
async save(sendData: [Send, EncArrayBuffer]): Promise<Send> {
|
||||
const response = await this.upload(sendData);
|
||||
|
||||
const data = new SendData(response);
|
||||
await this.sendService.upsert(data);
|
||||
return new Send(data);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<any> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<button bitButton [bitMenuTriggerFor]="itemOptions" buttonType="primary" type="button">
|
||||
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
|
||||
{{ "new" | i18n }}
|
||||
<i *ngIf="!hideIcon" class="bwi bwi-plus-f" aria-hidden="true"></i>
|
||||
{{ (hideIcon ? "createSend" : "new") | i18n }}
|
||||
</button>
|
||||
<bit-menu #itemOptions>
|
||||
<a type="button" bitMenuItem (click)="newItemNavigate(sendType.Text)">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { Router, RouterLink } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
@@ -15,6 +15,8 @@ import { BadgeModule, ButtonModule, MenuModule } from "@bitwarden/components";
|
||||
imports: [JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule, BadgeModule],
|
||||
})
|
||||
export class NewSendDropdownComponent implements OnInit {
|
||||
@Input() hideIcon: boolean = false;
|
||||
|
||||
sendType = SendType;
|
||||
|
||||
hasNoPremium = false;
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
bitIconButton="bwi-clone"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'copyPassword' | i18n"
|
||||
[disabled]="!sendOptionsForm.get('password').value"
|
||||
[valueLabel]="'password' | i18n"
|
||||
[appCopyClick]="sendOptionsForm.get('password').value"
|
||||
showToast
|
||||
|
||||
@@ -85,9 +85,14 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send
|
||||
submitBtn?: ButtonComponent;
|
||||
|
||||
/**
|
||||
* Event emitted when the send is saved successfully.
|
||||
* Event emitted when the send is created successfully.
|
||||
*/
|
||||
@Output() sendSaved = new EventEmitter<SendView>();
|
||||
@Output() onSendCreated = new EventEmitter<SendView>();
|
||||
|
||||
/**
|
||||
* Event emitted when the send is updated successfully.
|
||||
*/
|
||||
@Output() onSendUpdated = new EventEmitter<SendView>();
|
||||
|
||||
/**
|
||||
* The original send being edited or cloned. Null for add mode.
|
||||
@@ -200,22 +205,26 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send
|
||||
return;
|
||||
}
|
||||
|
||||
const sendView = await this.addEditFormService.saveSend(
|
||||
this.updatedSendView,
|
||||
this.file,
|
||||
this.config,
|
||||
);
|
||||
|
||||
if (this.config.mode === "add") {
|
||||
this.onSendCreated.emit(sendView);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Utils.isNullOrWhitespace(this.updatedSendView.password)) {
|
||||
this.updatedSendView.password = null;
|
||||
}
|
||||
|
||||
await this.addEditFormService.saveSend(this.updatedSendView, this.file, this.config);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t(
|
||||
this.config.mode === "edit" || this.config.mode === "partial-edit"
|
||||
? "editedItem"
|
||||
: "addedItem",
|
||||
),
|
||||
message: this.i18nService.t("editedItem"),
|
||||
});
|
||||
|
||||
this.sendSaved.emit(this.updatedSendView);
|
||||
this.onSendUpdated.emit(this.updatedSendView);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export class DefaultSendFormService implements SendFormService {
|
||||
|
||||
async saveSend(send: SendView, file: File | ArrayBuffer, config: SendFormConfig) {
|
||||
const sendData = await this.sendService.encrypt(send, file, send.password, null);
|
||||
return await this.sendApiService.save(sendData);
|
||||
const newSend = await this.sendApiService.save(sendData);
|
||||
return await this.decryptSend(newSend);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export class SendItemsService {
|
||||
);
|
||||
|
||||
/**
|
||||
* Observable that indicates whether the user's vault is empty.
|
||||
* Observable that indicates whether the user's send list is empty.
|
||||
*/
|
||||
emptyList$: Observable<boolean> = this._sendList$.pipe(map((sends) => !sends.length));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user