mirror of
https://github.com/bitwarden/browser
synced 2026-02-26 09:33:22 +00:00
Merge branch 'main' of github.com:bitwarden/clients into feature/PM-30737-Migrate-DeleteAccount
This commit is contained in:
@@ -3035,10 +3035,6 @@
|
||||
"custom": {
|
||||
"message": "Custom"
|
||||
},
|
||||
"sendPasswordDescV3": {
|
||||
"message": "Add an optional password for recipients to access this Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"createSend": {
|
||||
"message": "New Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
@@ -6144,5 +6140,9 @@
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
},
|
||||
"sendPasswordHelperText": {
|
||||
"message": "Individuals will need to enter the password to view this Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -28,7 +26,7 @@ export default class WebRequestBackground {
|
||||
this.webRequest.onAuthRequired.addListener(
|
||||
(async (
|
||||
details: chrome.webRequest.OnAuthRequiredDetails,
|
||||
callback: (response: chrome.webRequest.BlockingResponse) => void,
|
||||
callback: (response: chrome.webRequest.BlockingResponse | null) => void,
|
||||
) => {
|
||||
if (!details.url || this.pendingAuthRequests.has(details.requestId)) {
|
||||
if (callback) {
|
||||
@@ -51,16 +49,16 @@ export default class WebRequestBackground {
|
||||
);
|
||||
|
||||
this.webRequest.onCompleted.addListener((details) => this.completeAuthRequest(details), {
|
||||
urls: ["http://*/*"],
|
||||
urls: ["http://*/*", "https://*/*"],
|
||||
});
|
||||
this.webRequest.onErrorOccurred.addListener((details) => this.completeAuthRequest(details), {
|
||||
urls: ["http://*/*"],
|
||||
urls: ["http://*/*", "https://*/*"],
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveAuthCredentials(
|
||||
domain: string,
|
||||
success: (response: chrome.webRequest.BlockingResponse) => void,
|
||||
success: (response: chrome.webRequest.BlockingResponse | null) => void,
|
||||
// eslint-disable-next-line
|
||||
error: Function,
|
||||
) {
|
||||
@@ -82,7 +80,7 @@ export default class WebRequestBackground {
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(
|
||||
domain,
|
||||
activeUserId,
|
||||
null,
|
||||
undefined,
|
||||
UriMatchStrategy.Host,
|
||||
);
|
||||
if (ciphers == null || ciphers.length !== 1) {
|
||||
@@ -90,10 +88,17 @@ export default class WebRequestBackground {
|
||||
return;
|
||||
}
|
||||
|
||||
const username = ciphers[0].login?.username;
|
||||
const password = ciphers[0].login?.password;
|
||||
if (username == null || password == null) {
|
||||
error();
|
||||
return;
|
||||
}
|
||||
|
||||
success({
|
||||
authCredentials: {
|
||||
username: ciphers[0].login.username,
|
||||
password: ciphers[0].login.password,
|
||||
username,
|
||||
password,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
|
||||
4
apps/desktop/desktop_native/Cargo.lock
generated
4
apps/desktop/desktop_native/Cargo.lock
generated
@@ -512,9 +512,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.0"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
|
||||
@@ -27,7 +27,7 @@ ashpd = "=0.12.0"
|
||||
base64 = "=0.22.1"
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "a641316227227f8777fdf56ac9fa2d6b5f7fe662" }
|
||||
byteorder = "=1.5.0"
|
||||
bytes = "=1.11.0"
|
||||
bytes = "=1.11.1"
|
||||
cbc = "=0.1.2"
|
||||
chacha20poly1305 = "=0.10.1"
|
||||
core-foundation = "=0.10.1"
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
"message": "Send details",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendPasswordDescV3": {
|
||||
"message": "Add an optional password for recipients to access this Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendTypeTextToShare": {
|
||||
"message": "Text to share"
|
||||
},
|
||||
@@ -4590,5 +4586,9 @@
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
},
|
||||
"sendPasswordHelperText": {
|
||||
"message": "Individuals will need to enter the password to view this Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,65 +6,79 @@
|
||||
(onCipherClicked)="viewCipher($event)"
|
||||
(onCipherRightClicked)="viewCipherMenu($event)"
|
||||
(onAddCipher)="addCipher($event)"
|
||||
[showPremiumCallout]="showPremiumCallout$ | async"
|
||||
>
|
||||
</app-vault-items-v2>
|
||||
<div class="details" *ngIf="!!action">
|
||||
<app-vault-item-footer
|
||||
id="footer"
|
||||
#footer
|
||||
[cipher]="cipher"
|
||||
[action]="action"
|
||||
(onEdit)="editCipher($event)"
|
||||
(onRestore)="restoreCipher()"
|
||||
(onClone)="cloneCipher($event)"
|
||||
(onDelete)="deleteCipher()"
|
||||
(onCancel)="cancelCipher($event)"
|
||||
(onArchiveToggle)="refreshCurrentCipher()"
|
||||
[masterPasswordAlreadyPrompted]="cipherRepromptId === cipherId"
|
||||
></app-vault-item-footer>
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<div class="box">
|
||||
<app-cipher-view *ngIf="action === 'view'" [cipher]="cipher" [collections]="collections">
|
||||
</app-cipher-view>
|
||||
<vault-cipher-form
|
||||
#vaultForm
|
||||
*ngIf="action === 'add' || action === 'edit' || action === 'clone'"
|
||||
formId="cipherForm"
|
||||
[config]="config"
|
||||
(cipherSaved)="savedCipher($event)"
|
||||
[submitBtn]="footer?.submitBtn"
|
||||
(formStatusChange$)="formStatusChanged($event)"
|
||||
>
|
||||
<bit-item slot="attachment-button">
|
||||
<button
|
||||
bit-item-content
|
||||
type="button"
|
||||
(click)="openAttachmentsDialog()"
|
||||
[disabled]="formDisabled"
|
||||
@if (!!action) {
|
||||
<div class="details">
|
||||
<app-vault-item-footer
|
||||
id="footer"
|
||||
#footer
|
||||
[cipher]="cipher"
|
||||
[action]="action"
|
||||
(onEdit)="editCipher($event)"
|
||||
(onRestore)="restoreCipher()"
|
||||
(onClone)="cloneCipher($event)"
|
||||
(onDelete)="deleteCipher()"
|
||||
(onCancel)="cancelCipher($event)"
|
||||
(onArchiveToggle)="refreshCurrentCipher()"
|
||||
[masterPasswordAlreadyPrompted]="cipherRepromptId === cipherId"
|
||||
></app-vault-item-footer>
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<div class="box">
|
||||
@if (action === "view") {
|
||||
<app-cipher-view [cipher]="cipher" [collections]="collections"> </app-cipher-view>
|
||||
}
|
||||
@if (action === "add" || action === "edit" || action === "clone") {
|
||||
<vault-cipher-form
|
||||
#vaultForm
|
||||
formId="cipherForm"
|
||||
[config]="config"
|
||||
(cipherSaved)="savedCipher($event)"
|
||||
[submitBtn]="footer?.submitBtn"
|
||||
(formStatusChange$)="formStatusChanged($event)"
|
||||
>
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
{{ "attachments" | i18n }}
|
||||
<app-premium-badge></app-premium-badge>
|
||||
</div>
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-item>
|
||||
</vault-cipher-form>
|
||||
<bit-item slot="attachment-button">
|
||||
<button
|
||||
bit-item-content
|
||||
type="button"
|
||||
(click)="openAttachmentsDialog()"
|
||||
[disabled]="formDisabled"
|
||||
>
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
{{ "attachments" | i18n }}
|
||||
<app-premium-badge></app-premium-badge>
|
||||
</div>
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-item>
|
||||
</vault-cipher-form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="logo"
|
||||
class="logo"
|
||||
*ngIf="action !== 'add' && action !== 'edit' && action !== 'view' && action !== 'clone'"
|
||||
>
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<img class="logo-image" alt="Bitwarden" aria-hidden="true" />
|
||||
}
|
||||
@if (!["add", "edit", "view", "clone"].includes(action)) {
|
||||
<div id="logo" class="logo">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
@if (activeFilter.isArchived && !(hasArchivedCiphers$ | async)) {
|
||||
<bit-no-items [icon]="itemTypesIcon">
|
||||
<div slot="title">
|
||||
{{ "noItemsInArchive" | i18n }}
|
||||
</div>
|
||||
<p slot="description" bitTypography="body2" class="tw-max-w-md tw-text-center">
|
||||
{{ "noItemsInArchiveDesc" | i18n }}
|
||||
</p>
|
||||
</bit-no-items>
|
||||
} @else {
|
||||
<img class="logo-image" alt="Bitwarden" aria-hidden="true" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<ng-template #folderAddEdit></ng-template>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { filter, map, take } from "rxjs/operators";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
|
||||
import { ItemTypes } from "@bitwarden/assets/svg";
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@@ -58,6 +59,7 @@ import {
|
||||
ToastService,
|
||||
CopyClickListener,
|
||||
COPY_CLICK_LISTENER,
|
||||
NoItemsModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import {
|
||||
@@ -112,6 +114,7 @@ const BroadcasterSubscriptionId = "VaultComponent";
|
||||
ButtonModule,
|
||||
PremiumBadgeComponent,
|
||||
VaultItemsV2Component,
|
||||
NoItemsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -154,7 +157,7 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
type: CipherType | null = null;
|
||||
folderId: string | null | undefined = null;
|
||||
collectionId: string | null = null;
|
||||
organizationId: string | null = null;
|
||||
organizationId: OrganizationId | null = null;
|
||||
myVaultOnly = false;
|
||||
addType: CipherType | undefined = undefined;
|
||||
addOrganizationId: string | null = null;
|
||||
@@ -168,9 +171,19 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
cipher: CipherView | null = new CipherView();
|
||||
collections: CollectionView[] | null = null;
|
||||
config: CipherFormConfig | null = null;
|
||||
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
showPremiumCallout$: Observable<boolean> = this.userId$.pipe(
|
||||
switchMap((userId) =>
|
||||
combineLatest([
|
||||
this.routedVaultFilterBridgeService.activeFilter$,
|
||||
this.cipherArchiveService.showSubscriptionEndedMessaging$(userId),
|
||||
]).pipe(map(([activeFilter, showMessaging]) => activeFilter.isArchived && showMessaging)),
|
||||
),
|
||||
);
|
||||
|
||||
/** Tracks the disabled status of the edit cipher form */
|
||||
protected formDisabled: boolean = false;
|
||||
protected itemTypesIcon = ItemTypes;
|
||||
|
||||
private organizations$: Observable<Organization[]> = this.accountService.activeAccount$.pipe(
|
||||
map((a) => a?.id),
|
||||
@@ -178,10 +191,9 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
switchMap((id) => this.organizationService.organizations$(id)),
|
||||
);
|
||||
|
||||
protected canAccessAttachments$ = this.accountService.activeAccount$.pipe(
|
||||
filter((account): account is Account => !!account),
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||
protected hasArchivedCiphers$ = this.userId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.cipherArchiveService.archivedCiphers$(userId).pipe(map((ciphers) => ciphers.length > 0)),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angul
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
@@ -32,7 +31,6 @@ import { SearchBarService } from "../../../app/layout/search/search-bar.service"
|
||||
})
|
||||
export class VaultItemsV2Component<C extends CipherViewLike> extends BaseVaultItemsComponent<C> {
|
||||
readonly showPremiumCallout = input<boolean>(false);
|
||||
readonly organizationId = input<OrganizationId | undefined>(undefined);
|
||||
|
||||
protected CipherViewLikeUtils = CipherViewLikeUtils;
|
||||
|
||||
@@ -55,7 +53,7 @@ export class VaultItemsV2Component<C extends CipherViewLike> extends BaseVaultIt
|
||||
}
|
||||
|
||||
async navigateToGetPremium() {
|
||||
await this.premiumUpgradePromptService.promptForPremium(this.organizationId());
|
||||
await this.premiumUpgradePromptService.promptForPremium();
|
||||
}
|
||||
|
||||
trackByFn(index: number, c: C): string {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
(onCipherRightClicked)="viewCipherMenu($event)"
|
||||
(onAddCipher)="addCipher($event)"
|
||||
[showPremiumCallout]="showPremiumCallout$ | async"
|
||||
[organizationId]="organizationId"
|
||||
>
|
||||
</app-vault-items-v2>
|
||||
@if (!!action) {
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
|
||||
import {
|
||||
MasterPasswordAuthenticationData,
|
||||
MasterPasswordUnlockData,
|
||||
} from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
|
||||
// @ts-strict-ignore
|
||||
export class EmergencyAccessPasswordRequest {
|
||||
newMasterPasswordHash: string;
|
||||
key: string;
|
||||
|
||||
// This will eventually be changed to be an actual constructor, once all callers are updated.
|
||||
// The body of this request will be changed to carry the authentication data and unlock data.
|
||||
// https://bitwarden.atlassian.net/browse/PM-23234
|
||||
static newConstructor(
|
||||
authenticationData: MasterPasswordAuthenticationData,
|
||||
unlockData: MasterPasswordUnlockData,
|
||||
): EmergencyAccessPasswordRequest {
|
||||
const request = new EmergencyAccessPasswordRequest();
|
||||
request.newMasterPasswordHash = authenticationData.masterPasswordAuthenticationHash;
|
||||
request.key = unlockData.masterKeyWrappedUserKey;
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,17 @@ import { of } from "rxjs";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
MasterKeyWrappedUserKey,
|
||||
MasterPasswordAuthenticationData,
|
||||
MasterPasswordAuthenticationHash,
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -18,7 +27,13 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey, MasterKey, UserPrivateKey } from "@bitwarden/common/types/key";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
import { Argon2KdfConfig, KdfType, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
import {
|
||||
Argon2KdfConfig,
|
||||
DEFAULT_KDF_CONFIG,
|
||||
KdfType,
|
||||
KeyService,
|
||||
PBKDF2KdfConfig,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type";
|
||||
import { EmergencyAccessType } from "../enums/emergency-access-type";
|
||||
@@ -42,6 +57,8 @@ describe("EmergencyAccessService", () => {
|
||||
let cipherService: MockProxy<CipherService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let emergencyAccessService: EmergencyAccessService;
|
||||
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
const mockNewUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("trustedPublicKey")];
|
||||
@@ -54,6 +71,8 @@ describe("EmergencyAccessService", () => {
|
||||
encryptService = mock<EncryptService>();
|
||||
cipherService = mock<CipherService>();
|
||||
logService = mock<LogService>();
|
||||
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
emergencyAccessService = new EmergencyAccessService(
|
||||
emergencyAccessApiService,
|
||||
@@ -62,6 +81,8 @@ describe("EmergencyAccessService", () => {
|
||||
encryptService,
|
||||
cipherService,
|
||||
logService,
|
||||
masterPasswordService,
|
||||
configService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -215,7 +236,13 @@ describe("EmergencyAccessService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("takeover", () => {
|
||||
/**
|
||||
* @deprecated This 'describe' to be removed in PM-28143. When you remove this, check also if there are any imports/properties
|
||||
* in the test setup above that are now un-used and can also be removed.
|
||||
*/
|
||||
describe("takeover [PM27086_UpdateAuthenticationApisForInputPassword flag DISABLED]", () => {
|
||||
const PM27086_UpdateAuthenticationApisForInputPasswordEnabled = false;
|
||||
|
||||
const params = {
|
||||
id: "emergencyAccessId",
|
||||
masterPassword: "mockPassword",
|
||||
@@ -242,6 +269,10 @@ describe("EmergencyAccessService", () => {
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
configService.getFeatureFlag.mockResolvedValue(
|
||||
PM27086_UpdateAuthenticationApisForInputPasswordEnabled,
|
||||
);
|
||||
|
||||
emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce(takeoverResponse);
|
||||
keyService.userPrivateKey$.mockReturnValue(of(userPrivateKey));
|
||||
|
||||
@@ -450,6 +481,180 @@ describe("EmergencyAccessService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("takeover [PM27086_UpdateAuthenticationApisForInputPassword flag ENABLED]", () => {
|
||||
// Mock feature flag value
|
||||
const PM27086_UpdateAuthenticationApisForInputPasswordEnabled = true;
|
||||
|
||||
// Mock sut method params
|
||||
const id = "emergency-access-id";
|
||||
const masterPassword = "mockPassword";
|
||||
const email = "user@example.com";
|
||||
const activeUserId = newGuid() as UserId;
|
||||
|
||||
// Mock method data
|
||||
const kdfConfig = DEFAULT_KDF_CONFIG;
|
||||
|
||||
const takeoverResponse = {
|
||||
keyEncrypted: "EncryptedKey",
|
||||
kdf: kdfConfig.kdfType,
|
||||
kdfIterations: kdfConfig.iterations,
|
||||
} as EmergencyAccessTakeoverResponse;
|
||||
|
||||
const activeUserPrivateKey = new Uint8Array(64) as UserPrivateKey;
|
||||
let mockGrantorUserKey: UserKey;
|
||||
let salt: MasterPasswordSalt;
|
||||
let authenticationData: MasterPasswordAuthenticationData;
|
||||
let unlockData: MasterPasswordUnlockData;
|
||||
|
||||
beforeEach(() => {
|
||||
configService.getFeatureFlag.mockResolvedValue(
|
||||
PM27086_UpdateAuthenticationApisForInputPasswordEnabled,
|
||||
);
|
||||
|
||||
emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValue(takeoverResponse);
|
||||
keyService.userPrivateKey$.mockReturnValue(of(activeUserPrivateKey));
|
||||
|
||||
const mockDecryptedGrantorUserKey = new SymmetricCryptoKey(new Uint8Array(64));
|
||||
encryptService.decapsulateKeyUnsigned.mockResolvedValue(mockDecryptedGrantorUserKey);
|
||||
mockGrantorUserKey = mockDecryptedGrantorUserKey as UserKey;
|
||||
|
||||
salt = email as MasterPasswordSalt;
|
||||
masterPasswordService.emailToSalt.mockReturnValue(salt);
|
||||
|
||||
authenticationData = {
|
||||
salt,
|
||||
kdf: kdfConfig,
|
||||
masterPasswordAuthenticationHash:
|
||||
"masterPasswordAuthenticationHash" as MasterPasswordAuthenticationHash,
|
||||
};
|
||||
|
||||
unlockData = {
|
||||
salt,
|
||||
kdf: kdfConfig,
|
||||
masterKeyWrappedUserKey: "masterKeyWrappedUserKey" as MasterKeyWrappedUserKey,
|
||||
} as MasterPasswordUnlockData;
|
||||
|
||||
masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue(
|
||||
authenticationData,
|
||||
);
|
||||
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(unlockData);
|
||||
});
|
||||
|
||||
it("should throw if active user private key is not found", async () => {
|
||||
// Arrange
|
||||
keyService.userPrivateKey$.mockReturnValue(of(null));
|
||||
|
||||
// Act
|
||||
const promise = emergencyAccessService.takeover(id, masterPassword, email, activeUserId);
|
||||
|
||||
// Assert
|
||||
await expect(promise).rejects.toThrow(
|
||||
"Active user does not have a private key, cannot complete a takeover.",
|
||||
);
|
||||
expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw if the grantor user key cannot be decrypted via the active user private key", async () => {
|
||||
// Arrange
|
||||
encryptService.decapsulateKeyUnsigned.mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
const promise = emergencyAccessService.takeover(id, masterPassword, email, activeUserId);
|
||||
|
||||
// Assert
|
||||
await expect(promise).rejects.toThrow("Failed to decrypt grantor key");
|
||||
expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should use PBKDF2 if takeover response contains KdfType.PBKDF2_SHA256", async () => {
|
||||
// Act
|
||||
await emergencyAccessService.takeover(id, masterPassword, email, activeUserId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordService.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith(
|
||||
masterPassword,
|
||||
kdfConfig, // default config (PBKDF2)
|
||||
salt,
|
||||
);
|
||||
});
|
||||
|
||||
it("should use Argon2 if takeover response contains KdfType.Argon2id", async () => {
|
||||
// Arrange
|
||||
const argon2TakeoverResponse = {
|
||||
keyEncrypted: "EncryptedKey",
|
||||
kdf: KdfType.Argon2id,
|
||||
kdfIterations: 3,
|
||||
kdfMemory: 64,
|
||||
kdfParallelism: 4,
|
||||
} as EmergencyAccessTakeoverResponse;
|
||||
|
||||
emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValue(
|
||||
argon2TakeoverResponse,
|
||||
);
|
||||
|
||||
const expectedKdfConfig = new Argon2KdfConfig(
|
||||
argon2TakeoverResponse.kdfIterations,
|
||||
argon2TakeoverResponse.kdfMemory,
|
||||
argon2TakeoverResponse.kdfParallelism,
|
||||
);
|
||||
|
||||
// Act
|
||||
await emergencyAccessService.takeover(id, masterPassword, email, activeUserId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordService.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith(
|
||||
masterPassword,
|
||||
expectedKdfConfig,
|
||||
salt,
|
||||
);
|
||||
expect(masterPasswordService.makeMasterPasswordAuthenticationData).not.toHaveBeenCalledWith(
|
||||
masterPassword,
|
||||
kdfConfig, // default config (PBKDF2)
|
||||
salt,
|
||||
);
|
||||
});
|
||||
|
||||
it("should call makeMasterPasswordAuthenticationData and makeMasterPasswordUnlockData with the correct parameters", async () => {
|
||||
// Act
|
||||
await emergencyAccessService.takeover(id, masterPassword, email, activeUserId);
|
||||
|
||||
// Assert
|
||||
const request = EmergencyAccessPasswordRequest.newConstructor(authenticationData, unlockData);
|
||||
|
||||
expect(masterPasswordService.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith(
|
||||
masterPassword,
|
||||
kdfConfig,
|
||||
salt,
|
||||
);
|
||||
|
||||
expect(masterPasswordService.makeMasterPasswordUnlockData).toHaveBeenCalledWith(
|
||||
masterPassword,
|
||||
kdfConfig,
|
||||
salt,
|
||||
mockGrantorUserKey,
|
||||
);
|
||||
|
||||
expect(emergencyAccessApiService.postEmergencyAccessPassword).toHaveBeenCalledWith(
|
||||
id,
|
||||
request,
|
||||
);
|
||||
});
|
||||
|
||||
it("should call the API method to change the grantor's master password", async () => {
|
||||
// Act
|
||||
await emergencyAccessService.takeover(id, masterPassword, email, activeUserId);
|
||||
|
||||
// Assert
|
||||
const request = EmergencyAccessPasswordRequest.newConstructor(authenticationData, unlockData);
|
||||
|
||||
expect(emergencyAccessApiService.postEmergencyAccessPassword).toHaveBeenCalledTimes(1);
|
||||
expect(emergencyAccessApiService.postEmergencyAccessPassword).toHaveBeenCalledWith(
|
||||
id,
|
||||
request,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRotatedData", () => {
|
||||
const allowedStatuses = [
|
||||
EmergencyAccessStatusType.Confirmed,
|
||||
|
||||
@@ -4,11 +4,19 @@ import { firstValueFrom } from "rxjs";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import {
|
||||
EncryptedString,
|
||||
EncString,
|
||||
} from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
MasterPasswordAuthenticationData,
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -56,6 +64,8 @@ export class EmergencyAccessService implements UserKeyRotationKeyRecoveryProvide
|
||||
private encryptService: EncryptService,
|
||||
private cipherService: CipherService,
|
||||
private logService: LogService,
|
||||
private masterPasswordService: MasterPasswordServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -270,7 +280,7 @@ export class EmergencyAccessService implements UserKeyRotationKeyRecoveryProvide
|
||||
* Intended for grantee.
|
||||
* @param id emergency access id
|
||||
* @param masterPassword new master password
|
||||
* @param email email address of grantee (must be consistent or login will fail)
|
||||
* @param email email address of grantor (must be consistent or login will fail)
|
||||
* @param activeUserId the user id of the active user
|
||||
*/
|
||||
async takeover(id: string, masterPassword: string, email: string, activeUserId: UserId) {
|
||||
@@ -309,6 +319,36 @@ export class EmergencyAccessService implements UserKeyRotationKeyRecoveryProvide
|
||||
break;
|
||||
}
|
||||
|
||||
// When you unwind the flag in PM-28143, also remove the ConfigService if it is un-used.
|
||||
const newApisWithInputPasswordFlagEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM27086_UpdateAuthenticationApisForInputPassword,
|
||||
);
|
||||
|
||||
if (newApisWithInputPasswordFlagEnabled) {
|
||||
const salt: MasterPasswordSalt = this.masterPasswordService.emailToSalt(email);
|
||||
|
||||
const authenticationData: MasterPasswordAuthenticationData =
|
||||
await this.masterPasswordService.makeMasterPasswordAuthenticationData(
|
||||
masterPassword,
|
||||
config,
|
||||
salt,
|
||||
);
|
||||
|
||||
const unlockData: MasterPasswordUnlockData =
|
||||
await this.masterPasswordService.makeMasterPasswordUnlockData(
|
||||
masterPassword,
|
||||
config,
|
||||
salt,
|
||||
grantorUserKey,
|
||||
);
|
||||
|
||||
const request = EmergencyAccessPasswordRequest.newConstructor(authenticationData, unlockData);
|
||||
|
||||
await this.emergencyAccessApiService.postEmergencyAccessPassword(id, request);
|
||||
|
||||
return; // EARLY RETURN for flagged logic
|
||||
}
|
||||
|
||||
const masterKey = await this.keyService.makeMasterKey(masterPassword, email, config);
|
||||
const masterKeyHash = await this.keyService.hashMasterKey(masterPassword, masterKey);
|
||||
|
||||
|
||||
@@ -5645,10 +5645,6 @@
|
||||
"sendTypeText": {
|
||||
"message": "Text"
|
||||
},
|
||||
"sendPasswordDescV3": {
|
||||
"message": "Add an optional password for recipients to access this Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"createSend": {
|
||||
"message": "New Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
@@ -12782,5 +12778,12 @@
|
||||
},
|
||||
"invalidSendPassword": {
|
||||
"message": "Invalid Send password"
|
||||
},
|
||||
"sendPasswordHelperText": {
|
||||
"message": "Individuals will need to enter the password to view this Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"perUser": {
|
||||
"message": "per user"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user