mirror of
https://github.com/bitwarden/browser
synced 2026-02-09 05:00:10 +00:00
Merge branch 'main' into tools/pm-18793/port-credential-generator-service-to-providers
This commit is contained in:
@@ -284,7 +284,6 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected async approveFromOtherDevice() {
|
||||
this.loginEmailService.setLoginEmail(this.email);
|
||||
await this.router.navigate(["/login-with-device"]);
|
||||
}
|
||||
|
||||
@@ -297,7 +296,6 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected async requestAdminApproval() {
|
||||
this.loginEmailService.setLoginEmail(this.email);
|
||||
await this.router.navigate(["/admin-approval-requested"]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { IsActiveMatchOptions, Router, RouterModule } from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { Observable, filter, firstValueFrom, map, merge, race, take, timer } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -178,7 +178,26 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
private async initStandardAuthRequestFlow(): Promise<void> {
|
||||
this.flow = Flow.StandardAuthRequest;
|
||||
|
||||
this.email = (await firstValueFrom(this.loginEmailService.loginEmail$)) || undefined;
|
||||
// For a standard flow, we can get the user's email from two different places:
|
||||
// 1. The loginEmailService, which is the email that the user is trying to log in with. This is cleared
|
||||
// when the user logs in successfully. We can use this when the user is using Login with Device.
|
||||
// 2. With TDE Login with Another Device, the user is already logged in and we just need to get
|
||||
// a decryption key, so we can use the active account's email.
|
||||
const activeAccountEmail$: Observable<string | undefined> =
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email));
|
||||
const loginEmail$: Observable<string | null> = this.loginEmailService.loginEmail$;
|
||||
|
||||
// Use merge as we want to get the first value from either observable.
|
||||
const firstEmail$ = merge(loginEmail$, activeAccountEmail$).pipe(
|
||||
filter((e): e is string => !!e), // convert null/undefined to false and filter out so we narrow type to string
|
||||
take(1), // complete after first value
|
||||
);
|
||||
|
||||
const emailRetrievalTimeout$ = timer(2500).pipe(map(() => undefined as undefined));
|
||||
|
||||
// Wait for either the first email or the timeout to occur so we can proceed
|
||||
// neither above observable will complete, so we have to add a timeout
|
||||
this.email = await firstValueFrom(race(firstEmail$, emailRetrievalTimeout$));
|
||||
|
||||
if (!this.email) {
|
||||
await this.handleMissingEmail();
|
||||
|
||||
@@ -539,7 +539,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
// If we load an email into the form, we need to initialize it for the login process as well
|
||||
// so that other login components can use it.
|
||||
// We do this here as it's possible that a user doesn't edit the email field before submitting.
|
||||
this.loginEmailService.setLoginEmail(storedEmail);
|
||||
await this.loginEmailService.setLoginEmail(storedEmail);
|
||||
} else {
|
||||
this.formGroup.controls.rememberEmail.setValue(false);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export class PasswordHintComponent implements OnInit {
|
||||
};
|
||||
|
||||
protected async cancel() {
|
||||
this.loginEmailService.setLoginEmail(this.email);
|
||||
await this.loginEmailService.setLoginEmail(this.email);
|
||||
await this.router.navigate(["login"]);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ export enum FeatureFlag {
|
||||
|
||||
/* Billing */
|
||||
TrialPaymentOptional = "PM-8163-trial-payment",
|
||||
PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal",
|
||||
PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features",
|
||||
PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method",
|
||||
PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships",
|
||||
@@ -119,7 +118,6 @@ export const DefaultFeatureFlagValue = {
|
||||
|
||||
/* Billing */
|
||||
[FeatureFlag.TrialPaymentOptional]: FALSE,
|
||||
[FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE,
|
||||
[FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE,
|
||||
[FeatureFlag.PM18794_ProviderPaymentMethod]: FALSE,
|
||||
[FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE,
|
||||
|
||||
@@ -8,17 +8,99 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
|
||||
|
||||
export abstract class EncryptService {
|
||||
/**
|
||||
* @deprecated
|
||||
* Encrypts a string or Uint8Array to an EncString
|
||||
* @param plainValue - The value to encrypt
|
||||
* @param key - The key to encrypt the value with
|
||||
*/
|
||||
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
|
||||
/**
|
||||
* @deprecated
|
||||
* Encrypts a value to a Uint8Array
|
||||
* @param plainValue - The value to encrypt
|
||||
* @param key - The key to encrypt the value with
|
||||
*/
|
||||
abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>;
|
||||
/**
|
||||
* @deprecated
|
||||
* Decrypts an EncString to a string
|
||||
* @param encString - The EncString to decrypt
|
||||
* @param key - The key to decrypt the EncString with
|
||||
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
|
||||
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
|
||||
* @returns The decrypted string
|
||||
*/
|
||||
abstract decryptToUtf8(
|
||||
encString: EncString,
|
||||
key: SymmetricCryptoKey,
|
||||
decryptTrace?: string,
|
||||
): Promise<string>;
|
||||
/**
|
||||
* @deprecated
|
||||
* Decrypts an Encrypted object to a Uint8Array
|
||||
* @param encThing - The Encrypted object to decrypt
|
||||
* @param key - The key to decrypt the Encrypted object with
|
||||
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
|
||||
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
|
||||
* @returns The decrypted Uint8Array
|
||||
*/
|
||||
abstract decryptToBytes(
|
||||
encThing: Encrypted,
|
||||
key: SymmetricCryptoKey,
|
||||
decryptTrace?: string,
|
||||
): Promise<Uint8Array | null>;
|
||||
/**
|
||||
* @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed
|
||||
* @param items The items to decrypt
|
||||
* @param key The key to decrypt the items with
|
||||
*/
|
||||
abstract decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]>;
|
||||
|
||||
/**
|
||||
* Encrypts a string to an EncString
|
||||
* @param plainValue - The value to encrypt
|
||||
* @param key - The key to encrypt the value with
|
||||
*/
|
||||
abstract encryptString(plainValue: string, key: SymmetricCryptoKey): Promise<EncString>;
|
||||
/**
|
||||
* Encrypts bytes to an EncString
|
||||
* @param plainValue - The value to encrypt
|
||||
* @param key - The key to encrypt the value with
|
||||
* @deprecated Bytes are not the right abstraction to encrypt in. Use e.g. key wrapping or file encryption instead
|
||||
*/
|
||||
abstract encryptBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
|
||||
/**
|
||||
* Encrypts a value to a Uint8Array
|
||||
* @param plainValue - The value to encrypt
|
||||
* @param key - The key to encrypt the value with
|
||||
*/
|
||||
abstract encryptFileData(
|
||||
plainValue: Uint8Array,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<EncArrayBuffer>;
|
||||
|
||||
/**
|
||||
* Decrypts an EncString to a string
|
||||
* @param encString - The EncString containing the encrypted string.
|
||||
* @param key - The key to decrypt the value with
|
||||
*/
|
||||
abstract decryptString(encString: EncString, key: SymmetricCryptoKey): Promise<string>;
|
||||
/**
|
||||
* Decrypts an EncString to a Uint8Array
|
||||
* @param encString - The EncString containing the encrypted bytes.
|
||||
* @param key - The key to decrypt the value with
|
||||
* @deprecated Bytes are not the right abstraction to encrypt in. Use e.g. key wrapping or file encryption instead
|
||||
*/
|
||||
abstract decryptBytes(encString: EncString, key: SymmetricCryptoKey): Promise<Uint8Array>;
|
||||
/**
|
||||
* Decrypts an EncArrayBuffer to a Uint8Array
|
||||
* @param encBuffer - The EncArrayBuffer containing the encrypted file bytes.
|
||||
* @param key - The key to decrypt the value with
|
||||
*/
|
||||
abstract decryptFileData(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<Uint8Array>;
|
||||
|
||||
/**
|
||||
* Wraps a decapsulation key (Private key) with a symmetric key
|
||||
@@ -52,31 +134,35 @@ export abstract class EncryptService {
|
||||
): Promise<EncString>;
|
||||
|
||||
/**
|
||||
* Decrypts an EncString to a string
|
||||
* @param encString - The EncString to decrypt
|
||||
* @param key - The key to decrypt the EncString with
|
||||
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
|
||||
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
|
||||
* @returns The decrypted string
|
||||
* Unwraps a decapsulation key (Private key) with a symmetric key
|
||||
* @see {@link https://en.wikipedia.org/wiki/Key_wrap}
|
||||
* @param decapsulationKeyPcks8 - The private key in PKCS8 format
|
||||
* @param wrappingKey - The symmetric key to wrap the private key with
|
||||
*/
|
||||
abstract decryptToUtf8(
|
||||
encString: EncString,
|
||||
key: SymmetricCryptoKey,
|
||||
decryptTrace?: string,
|
||||
): Promise<string>;
|
||||
abstract unwrapDecapsulationKey(
|
||||
wrappedDecapsulationKey: EncString,
|
||||
wrappingKey: SymmetricCryptoKey,
|
||||
): Promise<Uint8Array>;
|
||||
/**
|
||||
* Decrypts an Encrypted object to a Uint8Array
|
||||
* @param encThing - The Encrypted object to decrypt
|
||||
* @param key - The key to decrypt the Encrypted object with
|
||||
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
|
||||
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
|
||||
* @returns The decrypted Uint8Array
|
||||
* Wraps an encapsulation key (Public key) with a symmetric key
|
||||
* @see {@link https://en.wikipedia.org/wiki/Key_wrap}
|
||||
* @param encapsulationKeySpki - The public key in SPKI format
|
||||
* @param wrappingKey - The symmetric key to wrap the public key with
|
||||
*/
|
||||
abstract decryptToBytes(
|
||||
encThing: Encrypted,
|
||||
key: SymmetricCryptoKey,
|
||||
decryptTrace?: string,
|
||||
): Promise<Uint8Array | null>;
|
||||
abstract unwrapEncapsulationKey(
|
||||
wrappedEncapsulationKey: EncString,
|
||||
wrappingKey: SymmetricCryptoKey,
|
||||
): Promise<Uint8Array>;
|
||||
/**
|
||||
* Unwraps a symmetric key with another symmetric key
|
||||
* @see {@link https://en.wikipedia.org/wiki/Key_wrap}
|
||||
* @param keyToBeWrapped - The symmetric key to wrap
|
||||
* @param wrappingKey - The symmetric key to wrap the encapsulated key with
|
||||
*/
|
||||
abstract unwrapSymmetricKey(
|
||||
keyToBeUnwrapped: EncString,
|
||||
wrappingKey: SymmetricCryptoKey,
|
||||
): Promise<SymmetricCryptoKey>;
|
||||
|
||||
/**
|
||||
* Encapsulates a symmetric key with an asymmetric public key
|
||||
@@ -100,6 +186,7 @@ export abstract class EncryptService {
|
||||
encryptedSharedKey: EncString,
|
||||
decapsulationKey: Uint8Array,
|
||||
): Promise<SymmetricCryptoKey>;
|
||||
|
||||
/**
|
||||
* @deprecated Use @see {@link encapsulateKeyUnsigned} instead
|
||||
* @param data - The data to encrypt
|
||||
@@ -112,15 +199,7 @@ export abstract class EncryptService {
|
||||
* @param privateKey - The privateKey to decrypt with
|
||||
*/
|
||||
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
|
||||
/**
|
||||
* @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed
|
||||
* @param items The items to decrypt
|
||||
* @param key The key to decrypt the items with
|
||||
*/
|
||||
abstract decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]>;
|
||||
|
||||
/**
|
||||
* Generates a base64-encoded hash of the given value
|
||||
* @param value The value to hash
|
||||
|
||||
@@ -36,31 +36,29 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
protected logMacFailures: boolean,
|
||||
) {}
|
||||
|
||||
// Handle updating private properties to turn on/off feature flags.
|
||||
onServerConfigChange(newConfig: ServerConfig): void {
|
||||
this.blockType0 = getFeatureFlagValue(newConfig, FeatureFlag.PM17987_BlockType0);
|
||||
// Proxy functions; Their implementation are temporary before moving at this level to the SDK
|
||||
async encryptString(plainValue: string, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
return this.encrypt(plainValue, key);
|
||||
}
|
||||
|
||||
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
async encryptBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
return this.encrypt(plainValue, key);
|
||||
}
|
||||
|
||||
if (this.blockType0) {
|
||||
if (key.inner().type === EncryptionType.AesCbc256_B64) {
|
||||
throw new Error("Type 0 encryption is not supported.");
|
||||
}
|
||||
}
|
||||
async encryptFileData(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||
return this.encryptToBytes(plainValue, key);
|
||||
}
|
||||
|
||||
if (plainValue == null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
async decryptString(encString: EncString, key: SymmetricCryptoKey): Promise<string> {
|
||||
return this.decryptToUtf8(encString, key);
|
||||
}
|
||||
|
||||
if (typeof plainValue === "string") {
|
||||
return this.encryptUint8Array(Utils.fromUtf8ToArray(plainValue), key);
|
||||
} else {
|
||||
return this.encryptUint8Array(plainValue, key);
|
||||
}
|
||||
async decryptBytes(encString: EncString, key: SymmetricCryptoKey): Promise<Uint8Array> {
|
||||
return this.decryptToBytes(encString, key);
|
||||
}
|
||||
|
||||
async decryptFileData(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<Uint8Array> {
|
||||
return this.decryptToBytes(encBuffer, key);
|
||||
}
|
||||
|
||||
async wrapDecapsulationKey(
|
||||
@@ -108,6 +106,57 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
return await this.encryptUint8Array(keyToBeWrapped.toEncoded(), wrappingKey);
|
||||
}
|
||||
|
||||
async unwrapDecapsulationKey(
|
||||
wrappedDecapsulationKey: EncString,
|
||||
wrappingKey: SymmetricCryptoKey,
|
||||
): Promise<Uint8Array> {
|
||||
return this.decryptBytes(wrappedDecapsulationKey, wrappingKey);
|
||||
}
|
||||
async unwrapEncapsulationKey(
|
||||
wrappedEncapsulationKey: EncString,
|
||||
wrappingKey: SymmetricCryptoKey,
|
||||
): Promise<Uint8Array> {
|
||||
return this.decryptBytes(wrappedEncapsulationKey, wrappingKey);
|
||||
}
|
||||
async unwrapSymmetricKey(
|
||||
keyToBeUnwrapped: EncString,
|
||||
wrappingKey: SymmetricCryptoKey,
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
return new SymmetricCryptoKey(await this.decryptBytes(keyToBeUnwrapped, wrappingKey));
|
||||
}
|
||||
|
||||
async hash(value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512"): Promise<string> {
|
||||
const hashArray = await this.cryptoFunctionService.hash(value, algorithm);
|
||||
return Utils.fromBufferToB64(hashArray);
|
||||
}
|
||||
|
||||
// Handle updating private properties to turn on/off feature flags.
|
||||
onServerConfigChange(newConfig: ServerConfig): void {
|
||||
this.blockType0 = getFeatureFlagValue(newConfig, FeatureFlag.PM17987_BlockType0);
|
||||
}
|
||||
|
||||
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
|
||||
if (this.blockType0) {
|
||||
if (key.inner().type === EncryptionType.AesCbc256_B64) {
|
||||
throw new Error("Type 0 encryption is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
if (plainValue == null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (typeof plainValue === "string") {
|
||||
return this.encryptUint8Array(Utils.fromUtf8ToArray(plainValue), key);
|
||||
} else {
|
||||
return this.encryptUint8Array(plainValue, key);
|
||||
}
|
||||
}
|
||||
|
||||
private async encryptUint8Array(
|
||||
plainValue: Uint8Array,
|
||||
key: SymmetricCryptoKey,
|
||||
@@ -339,11 +388,6 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
return results;
|
||||
}
|
||||
|
||||
async hash(value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512"): Promise<string> {
|
||||
const hashArray = await this.cryptoFunctionService.hash(value, algorithm);
|
||||
return Utils.fromBufferToB64(hashArray);
|
||||
}
|
||||
|
||||
private async aesEncrypt(data: Uint8Array, key: Aes256CbcHmacKey): Promise<EncryptedObject> {
|
||||
const obj = new EncryptedObject();
|
||||
obj.iv = await this.cryptoFunctionService.randomBytes(16);
|
||||
|
||||
@@ -538,6 +538,98 @@ describe("EncryptService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptString", () => {
|
||||
it("is a proxy to encrypt", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const plainValue = "data";
|
||||
encryptService.encrypt = jest.fn();
|
||||
await encryptService.encryptString(plainValue, key);
|
||||
expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptBytes", () => {
|
||||
it("is a proxy to encrypt", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const plainValue = makeStaticByteArray(16, 1);
|
||||
encryptService.encrypt = jest.fn();
|
||||
await encryptService.encryptBytes(plainValue, key);
|
||||
expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptFileData", () => {
|
||||
it("is a proxy to encryptToBytes", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const plainValue = makeStaticByteArray(16, 1);
|
||||
encryptService.encryptToBytes = jest.fn();
|
||||
await encryptService.encryptFileData(plainValue, key);
|
||||
expect(encryptService.encryptToBytes).toHaveBeenCalledWith(plainValue, key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptString", () => {
|
||||
it("is a proxy to decryptToUtf8", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
||||
encryptService.decryptToUtf8 = jest.fn();
|
||||
await encryptService.decryptString(encString, key);
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptBytes", () => {
|
||||
it("is a proxy to decryptToBytes", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
||||
encryptService.decryptToBytes = jest.fn();
|
||||
await encryptService.decryptBytes(encString, key);
|
||||
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptFileData", () => {
|
||||
it("is a proxy to decrypt", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncArrayBuffer(makeStaticByteArray(60, EncryptionType.AesCbc256_B64));
|
||||
encryptService.decryptToBytes = jest.fn();
|
||||
await encryptService.decryptFileData(encString, key);
|
||||
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unwrapDecapsulationKey", () => {
|
||||
it("is a proxy to decryptBytes", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
||||
encryptService.decryptBytes = jest.fn();
|
||||
await encryptService.unwrapDecapsulationKey(encString, key);
|
||||
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unwrapEncapsulationKey", () => {
|
||||
it("is a proxy to decryptBytes", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
||||
encryptService.decryptBytes = jest.fn();
|
||||
await encryptService.unwrapEncapsulationKey(encString, key);
|
||||
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unwrapSymmetricKey", () => {
|
||||
it("is a proxy to decryptBytes", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
||||
const jestFn = jest.fn();
|
||||
jestFn.mockResolvedValue(new Uint8Array(64));
|
||||
encryptService.decryptBytes = jestFn;
|
||||
await encryptService.unwrapSymmetricKey(encString, key);
|
||||
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rsa", () => {
|
||||
const data = makeStaticByteArray(64, 100);
|
||||
const testKey = new SymmetricCryptoKey(data);
|
||||
|
||||
@@ -3,6 +3,8 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -86,6 +88,14 @@ describe("CipherAttachmentsComponent", () => {
|
||||
provide: AccountService,
|
||||
useValue: accountService,
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useValue: mock<ApiService>(),
|
||||
},
|
||||
{
|
||||
provide: OrganizationService,
|
||||
useValue: mock<OrganizationService>(),
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(CipherAttachmentsComponent, {
|
||||
@@ -234,7 +244,21 @@ describe("CipherAttachmentsComponent", () => {
|
||||
it("calls `saveAttachmentWithServer`", async () => {
|
||||
await component.submit();
|
||||
|
||||
expect(saveAttachmentWithServer).toHaveBeenCalledWith(cipherDomain, file, mockUserId);
|
||||
expect(saveAttachmentWithServer).toHaveBeenCalledWith(
|
||||
cipherDomain,
|
||||
file,
|
||||
mockUserId,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("calls `saveAttachmentWithServer` with isAdmin=true when using admin API", async () => {
|
||||
// Set isAdmin to true to use admin API
|
||||
Object.defineProperty(component, "isAdmin", { value: true });
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(saveAttachmentWithServer).toHaveBeenCalledWith(cipherDomain, file, mockUserId, true);
|
||||
});
|
||||
|
||||
it("resets form and input values", async () => {
|
||||
|
||||
@@ -24,12 +24,16 @@ import {
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -82,6 +86,9 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
/** The `id` of the cipher in context */
|
||||
@Input({ required: true }) cipherId: CipherId;
|
||||
|
||||
/** The organization ID if this cipher belongs to an organization */
|
||||
@Input() organizationId?: OrganizationId;
|
||||
|
||||
/** An optional submit button, whose loading/disabled state will be tied to the form state. */
|
||||
@Input() submitBtn?: ButtonComponent;
|
||||
|
||||
@@ -99,6 +106,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
|
||||
private cipherDomain: Cipher;
|
||||
private activeUserId: UserId;
|
||||
private isAdmin = false;
|
||||
private destroy$ = inject(DestroyRef);
|
||||
|
||||
constructor(
|
||||
@@ -108,6 +116,8 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
private logService: LogService,
|
||||
private toastService: ToastService,
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private organizationService: OrganizationService,
|
||||
) {
|
||||
this.attachmentForm.statusChanges.pipe(takeUntilDestroyed()).subscribe((status) => {
|
||||
if (!this.submitBtn) {
|
||||
@@ -120,7 +130,8 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.cipherDomain = await this.cipherService.get(this.cipherId, this.activeUserId);
|
||||
this.cipherDomain = await this.getCipher(this.cipherId);
|
||||
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, this.activeUserId),
|
||||
);
|
||||
@@ -190,6 +201,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
this.cipherDomain,
|
||||
file,
|
||||
this.activeUserId,
|
||||
this.isAdmin,
|
||||
);
|
||||
|
||||
// re-decrypt the cipher to update the attachments
|
||||
@@ -223,4 +235,50 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
|
||||
this.onRemoveSuccess.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a cipher using the appropriate method based on user permissions.
|
||||
* If the user doesn't have direct access, but has organization admin access,
|
||||
* it will retrieve the cipher using the admin endpoint.
|
||||
*/
|
||||
private async getCipher(id: CipherId): Promise<Cipher | null> {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// First try to get the cipher directly with user permissions
|
||||
const localCipher = await this.cipherService.get(id, this.activeUserId);
|
||||
|
||||
// If we got the cipher or there's no organization context, return the result
|
||||
if (localCipher != null || !this.organizationId) {
|
||||
return localCipher;
|
||||
}
|
||||
|
||||
// Get the organization to check admin permissions
|
||||
const organization = await this.getOrganization();
|
||||
// Only try the admin API if the user has admin permissions
|
||||
if (organization != null && organization.canEditAllCiphers) {
|
||||
this.isAdmin = true;
|
||||
const cipherResponse = await this.apiService.getCipherAdmin(id);
|
||||
const cipherData = new CipherData(cipherResponse);
|
||||
return new Cipher(cipherData);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the organization for the given organization ID
|
||||
*/
|
||||
private async getOrganization(): Promise<Organization | null> {
|
||||
if (!this.organizationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const organizations = await firstValueFrom(
|
||||
this.organizationService.organizations$(this.activeUserId),
|
||||
);
|
||||
|
||||
return organizations.find((o) => o.id === this.organizationId) || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<app-cipher-attachments
|
||||
*ngIf="cipherId"
|
||||
[cipherId]="cipherId"
|
||||
[organizationId]="organizationId"
|
||||
[submitBtn]="submitBtn"
|
||||
(onUploadSuccess)="uploadSuccessful()"
|
||||
(onRemoveSuccess)="removalSuccessful()"
|
||||
|
||||
@@ -2,10 +2,12 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DIALOG_DATA, DialogRef } from "@bitwarden/components";
|
||||
|
||||
@@ -20,8 +22,10 @@ describe("AttachmentsV2Component", () => {
|
||||
let fixture: ComponentFixture<AttachmentsV2Component>;
|
||||
|
||||
const mockCipherId: CipherId = "cipher-id" as CipherId;
|
||||
const mockOrganizationId: OrganizationId = "organization-id" as OrganizationId;
|
||||
const mockParams: AttachmentsDialogParams = {
|
||||
cipherId: mockCipherId,
|
||||
organizationId: mockOrganizationId,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -34,6 +38,8 @@ describe("AttachmentsV2Component", () => {
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{ provide: AccountService, useValue: mock<AccountService>() },
|
||||
{ provide: ApiService, useValue: mock<ApiService>() },
|
||||
{ provide: OrganizationService, useValue: mock<OrganizationService>() },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
@@ -42,9 +48,10 @@ describe("AttachmentsV2Component", () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("initializes without errors and with the correct cipherId", () => {
|
||||
it("initializes without errors and with the correct cipherId and organizationId", () => {
|
||||
expect(component).toBeTruthy();
|
||||
expect(component.cipherId).toBe(mockParams.cipherId);
|
||||
expect(component.organizationId).toBe(mockParams.organizationId);
|
||||
});
|
||||
|
||||
it("closes the dialog with 'uploaded' result on uploadSuccessful", () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
@@ -17,6 +17,7 @@ import { CipherAttachmentsComponent } from "../../cipher-form/components/attachm
|
||||
|
||||
export interface AttachmentsDialogParams {
|
||||
cipherId: CipherId;
|
||||
organizationId?: OrganizationId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,6 +44,7 @@ export interface AttachmentDialogCloseResult {
|
||||
})
|
||||
export class AttachmentsV2Component {
|
||||
cipherId: CipherId;
|
||||
organizationId?: OrganizationId;
|
||||
attachmentFormId = CipherAttachmentsComponent.attachmentFormID;
|
||||
|
||||
/**
|
||||
@@ -55,6 +57,7 @@ export class AttachmentsV2Component {
|
||||
@Inject(DIALOG_DATA) public params: AttachmentsDialogParams,
|
||||
) {
|
||||
this.cipherId = params.cipherId;
|
||||
this.organizationId = params.organizationId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user