1
0
mirror of https://github.com/bitwarden/browser synced 2026-03-02 03:21:19 +00:00

Merge branch 'tools/pm-23531/rename-send-created-to-active-send-icon' into tools/pm-21776/update-send-access-copy

This commit is contained in:
✨ Audrey ✨
2025-07-08 08:59:52 -04:00
418 changed files with 12407 additions and 2815 deletions

View File

@@ -1,119 +1 @@
import { MockProxy, mock } from "jest-mock-extended";
import { Subject } from "rxjs";
import {
AbstractStorageService,
ObservableStorageService,
StorageUpdate,
} from "../src/platform/abstractions/storage.service";
import { StorageOptions } from "../src/platform/models/domain/storage-options";
const INTERNAL_KEY = "__internal__";
export class FakeStorageService implements AbstractStorageService, ObservableStorageService {
private store: Record<string, unknown>;
private updatesSubject = new Subject<StorageUpdate>();
private _valuesRequireDeserialization = false;
/**
* Returns a mock of a {@see AbstractStorageService} for asserting the expected
* amount of calls. It is not recommended to use this to mock implementations as
* they are not respected.
*/
mock: MockProxy<AbstractStorageService>;
constructor(initial?: Record<string, unknown>) {
this.store = initial ?? {};
this.mock = mock<AbstractStorageService>();
}
/**
* Updates the internal store for this fake implementation, this bypasses any mock calls
* or updates to the {@link updates$} observable.
* @param store
*/
internalUpdateStore(store: Record<string, unknown>) {
this.store = store;
}
get internalStore() {
return this.store;
}
internalUpdateValuesRequireDeserialization(value: boolean) {
this._valuesRequireDeserialization = value;
}
get valuesRequireDeserialization(): boolean {
return this._valuesRequireDeserialization;
}
get updates$() {
return this.updatesSubject.asObservable();
}
get<T>(key: string, options?: StorageOptions): Promise<T> {
// 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.mock.get(key, options);
const value = this.store[key] as T;
return Promise.resolve(value);
}
has(key: string, options?: StorageOptions): Promise<boolean> {
// 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.mock.has(key, options);
return Promise.resolve(this.store[key] != null);
}
async save<T>(key: string, obj: T, options?: StorageOptions): Promise<void> {
// These exceptions are copied from https://github.com/sindresorhus/conf/blob/608adb0c46fb1680ddbd9833043478367a64c120/source/index.ts#L193-L203
// which is a library that is used by `ElectronStorageService`. We add them here to ensure that the behavior in our testing mirrors the real world.
if (typeof key !== "string" && typeof key !== "object") {
throw new TypeError(
`Expected \`key\` to be of type \`string\` or \`object\`, got ${typeof key}`,
);
}
// We don't throw this error because ElectronStorageService automatically detects this case
// and calls `delete()` instead of `set()`.
// if (typeof key !== "object" && obj === undefined) {
// throw new TypeError("Use `delete()` to clear values");
// }
if (this._containsReservedKey(key)) {
throw new TypeError(
`Please don't use the ${INTERNAL_KEY} key, as it's used to manage this module internal operations.`,
);
}
// 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.mock.save(key, obj, options);
this.store[key] = obj;
this.updatesSubject.next({ key: key, updateType: "save" });
}
remove(key: string, options?: StorageOptions): Promise<void> {
// 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.mock.remove(key, options);
delete this.store[key];
this.updatesSubject.next({ key: key, updateType: "remove" });
return Promise.resolve();
}
private _containsReservedKey(key: string | Partial<unknown>): boolean {
if (typeof key === "object") {
const firsKey = Object.keys(key)[0];
if (firsKey === INTERNAL_KEY) {
return true;
}
}
if (typeof key !== "string") {
return false;
}
return false;
}
}
export { FakeStorageService } from "@bitwarden/storage-test-utils";

View File

@@ -1,9 +1,11 @@
import { ClientSettings, LogLevel, BitwardenClient } from "@bitwarden/sdk-internal";
import { BitwardenClient } from "@bitwarden/sdk-internal";
import { SdkClientFactory } from "../src/platform/abstractions/sdk/sdk-client-factory";
export class DefaultSdkClientFactory implements SdkClientFactory {
createSdkClient(settings?: ClientSettings, log_level?: LogLevel): Promise<BitwardenClient> {
createSdkClient(
...args: ConstructorParameters<typeof BitwardenClient>
): Promise<BitwardenClient> {
throw new Error("Method not implemented.");
}
}

View File

@@ -0,0 +1,45 @@
/**
* Asserts that a value is non-nullish (not `null` or `undefined`); throws if value is nullish.
*
* @param val the value to check
* @param name the name of the value to include in the error message
* @param ctx context to optionally append to the error message
* @throws if the value is null or undefined
*
* @example
*
* ```
* // `newPasswordHint` can have an empty string as a valid value, so we check non-nullish
* this.assertNonNullish(
* passwordInputResult.newPasswordHint,
* "newPasswordHint",
* "Could not set initial password."
* );
* // Output error message: "newPasswordHint is null or undefined. Could not set initial password."
* ```
*
* @remarks
*
* If you use this method repeatedly to check several values, it may help to assign any
* additional context (`ctx`) to a variable and pass it in to each call. This prevents the
* call from reformatting vertically via prettier in your text editor, taking up multiple lines.
*
* For example:
* ```
* const ctx = "Could not set initial password.";
*
* this.assertNonNullish(valueOne, "valueOne", ctx);
* this.assertNonNullish(valueTwo, "valueTwo", ctx);
* this.assertNonNullish(valueThree, "valueThree", ctx);
* ```
*/
export function assertNonNullish<T>(
val: T,
name: string,
ctx?: string,
): asserts val is NonNullable<T> {
if (val == null) {
// If context is provided, append it to the error message with a space before it.
throw new Error(`${name} is null or undefined.${ctx ? ` ${ctx}` : ""}`);
}
}

View File

@@ -0,0 +1,46 @@
/**
* Asserts that a value is truthy; throws if value is falsy.
*
* @param val the value to check
* @param name the name of the value to include in the error message
* @param ctx context to optionally append to the error message
* @throws if the value is falsy (`false`, `""`, `0`, `null`, `undefined`, `void`, or `NaN`)
*
* @example
*
* ```
* this.assertTruthy(
* this.organizationId,
* "organizationId",
* "Could not set initial password."
* );
* // Output error message: "organizationId is falsy. Could not set initial password."
* ```
*
* @remarks
*
* If you use this method repeatedly to check several values, it may help to assign any
* additional context (`ctx`) to a variable and pass it in to each call. This prevents the
* call from reformatting vertically via prettier in your text editor, taking up multiple lines.
*
* For example:
* ```
* const ctx = "Could not set initial password.";
*
* this.assertTruthy(valueOne, "valueOne", ctx);
* this.assertTruthy(valueTwo, "valueTwo", ctx);
* this.assertTruthy(valueThree, "valueThree", ctx);
*/
export function assertTruthy<T>(
val: T,
name: string,
ctx?: string,
): asserts val is Exclude<T, false | "" | 0 | null | undefined | void | 0n> {
// Because `NaN` is a value (not a type) of type 'number', that means we cannot add
// it to the list of falsy values in the type assertion. Instead, we check for it
// separately at runtime.
if (!val || (typeof val === "number" && Number.isNaN(val))) {
// If context is provided, append it to the error message with a space before it.
throw new Error(`${name} is falsy.${ctx ? ` ${ctx}` : ""}`);
}
}

View File

@@ -0,0 +1,2 @@
export { assertTruthy } from "./assert-truthy.util";
export { assertNonNullish } from "./assert-non-nullish.util";

View File

@@ -27,6 +27,7 @@ export enum DeviceType {
WindowsCLI = 23,
MacOsCLI = 24,
LinuxCLI = 25,
DuckDuckGoBrowser = 26,
}
/**
@@ -55,6 +56,7 @@ export const DeviceTypeMetadata: Record<DeviceType, DeviceTypeMetadata> = {
[DeviceType.IEBrowser]: { category: "webVault", platform: "IE" },
[DeviceType.SafariBrowser]: { category: "webVault", platform: "Safari" },
[DeviceType.VivaldiBrowser]: { category: "webVault", platform: "Vivaldi" },
[DeviceType.DuckDuckGoBrowser]: { category: "webVault", platform: "DuckDuckGo" },
[DeviceType.UnknownBrowser]: { category: "webVault", platform: "Unknown" },
[DeviceType.WindowsDesktop]: { category: "desktop", platform: "Windows" },
[DeviceType.MacOsDesktop]: { category: "desktop", platform: "macOS" },

View File

@@ -90,4 +90,7 @@ export enum EventType {
OrganizationDomain_NotVerified = 2003,
Secret_Retrieved = 2100,
Secret_Created = 2101,
Secret_Edited = 2102,
Secret_Deleted = 2103,
}

View File

@@ -33,6 +33,7 @@ export enum FeatureFlag {
PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships",
PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup",
UseOrganizationWarningsService = "use-organization-warnings-service",
AllowTrialLengthZero = "pm-20322-allow-trial-length-0",
/* Data Insights and Reporting */
EnableRiskInsightsNotifications = "enable-risk-insights-notifications",
@@ -55,6 +56,7 @@ export enum FeatureFlag {
PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms",
EndUserNotifications = "pm-10609-end-user-notifications",
RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy",
PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp",
/* Platform */
IpcChannelFramework = "ipc-channel-framework",
@@ -99,6 +101,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.EndUserNotifications]: FALSE,
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
[FeatureFlag.RemoveCardItemTypePolicy]: FALSE,
[FeatureFlag.PM19315EndUserActivationMvp]: FALSE,
/* Auth */
[FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE,
@@ -112,6 +115,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE,
[FeatureFlag.PM19956_RequireProviderPaymentMethodDuringSetup]: FALSE,
[FeatureFlag.UseOrganizationWarningsService]: FALSE,
[FeatureFlag.AllowTrialLengthZero]: FALSE,
/* Key Management */
[FeatureFlag.PrivateKeyRegeneration]: FALSE,

View File

@@ -29,5 +29,5 @@ export enum NotificationType {
Notification = 20,
NotificationStatus = 21,
PendingSecurityTasks = 22,
RefreshSecurityTasks = 22,
}

View File

@@ -124,9 +124,9 @@ export class CipherExport {
domain.passwordHistory = req.passwordHistory.map((ph) => PasswordHistoryExport.toDomain(ph));
}
domain.creationDate = req.creationDate;
domain.revisionDate = req.revisionDate;
domain.deletedDate = req.deletedDate;
domain.creationDate = req.creationDate ? new Date(req.creationDate) : null;
domain.revisionDate = req.revisionDate ? new Date(req.revisionDate) : null;
domain.deletedDate = req.deletedDate ? new Date(req.deletedDate) : null;
return domain;
}

View File

@@ -22,7 +22,7 @@ export class PasswordHistoryExport {
static toDomain(req: PasswordHistoryExport, domain = new Password()) {
domain.password = req.password != null ? new EncString(req.password) : null;
domain.lastUsedDate = req.lastUsedDate;
domain.lastUsedDate = req.lastUsedDate ? new Date(req.lastUsedDate) : null;
return domain;
}

View File

@@ -70,7 +70,7 @@ export class Fido2AuthenticatorError extends Error {
}
export interface PublicKeyCredentialDescriptor {
id: Uint8Array;
id: ArrayBuffer;
transports?: ("ble" | "hybrid" | "internal" | "nfc" | "usb")[];
type: "public-key";
}
@@ -155,9 +155,9 @@ export interface Fido2AuthenticatorGetAssertionParams {
export interface Fido2AuthenticatorGetAssertionResult {
selectedCredential: {
id: Uint8Array;
userHandle?: Uint8Array;
id: ArrayBuffer;
userHandle?: ArrayBuffer;
};
authenticatorData: Uint8Array;
signature: Uint8Array;
authenticatorData: ArrayBuffer;
signature: ArrayBuffer;
}

View File

@@ -1,9 +1 @@
import { LogLevelType } from "../enums/log-level-type.enum";
export abstract class LogService {
abstract debug(message?: any, ...optionalParams: any[]): void;
abstract info(message?: any, ...optionalParams: any[]): void;
abstract warning(message?: any, ...optionalParams: any[]): void;
abstract error(message?: any, ...optionalParams: any[]): void;
abstract write(level: LogLevelType, message?: any, ...optionalParams: any[]): void;
}
export { LogService } from "@bitwarden/logging";

View File

@@ -28,6 +28,15 @@ export abstract class PlatformUtilsService {
abstract getApplicationVersionNumber(): Promise<string>;
abstract supportsWebAuthn(win: Window): boolean;
abstract supportsDuo(): boolean;
/**
* Returns true if the device supports autofill functionality
*/
abstract supportsAutofill(): boolean;
/**
* Returns true if the device supports native file downloads without
* the need for `target="_blank"`
*/
abstract supportsFileDownloads(): boolean;
/**
* @deprecated use `@bitwarden/components/ToastService.showToast` instead
*

View File

@@ -1,8 +1 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum LogLevelType {
Debug,
Info,
Warning,
Error,
}
export { LogLevel as LogLevelType } from "@bitwarden/logging";

View File

@@ -31,22 +31,35 @@ export type TimeoutManager = {
class SignalRLogger implements ILogger {
constructor(private readonly logService: LogService) {}
redactMessage(message: string): string {
const ACCESS_TOKEN_TEXT = "access_token=";
// Redact the access token from the logs if it exists.
const accessTokenIndex = message.indexOf(ACCESS_TOKEN_TEXT);
if (accessTokenIndex !== -1) {
return message.substring(0, accessTokenIndex + ACCESS_TOKEN_TEXT.length) + "[REDACTED]";
}
return message;
}
log(logLevel: LogLevel, message: string): void {
const redactedMessage = `[SignalR] ${this.redactMessage(message)}`;
switch (logLevel) {
case LogLevel.Critical:
this.logService.error(message);
this.logService.error(redactedMessage);
break;
case LogLevel.Error:
this.logService.error(message);
this.logService.error(redactedMessage);
break;
case LogLevel.Warning:
this.logService.warning(message);
this.logService.warning(redactedMessage);
break;
case LogLevel.Information:
this.logService.info(message);
this.logService.info(redactedMessage);
break;
case LogLevel.Debug:
this.logService.debug(message);
this.logService.debug(redactedMessage);
break;
}
}

View File

@@ -1,6 +1,6 @@
import { interceptConsole, restoreConsole } from "../../../spec";
import { ConsoleLogService } from "@bitwarden/logging";
import { ConsoleLogService } from "./console-log.service";
import { interceptConsole, restoreConsole } from "../../../spec";
describe("ConsoleLogService", () => {
const error = new Error("this is an error");

View File

@@ -1,59 +1 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { LogService as LogServiceAbstraction } from "../abstractions/log.service";
import { LogLevelType } from "../enums/log-level-type.enum";
export class ConsoleLogService implements LogServiceAbstraction {
protected timersMap: Map<string, [number, number]> = new Map();
constructor(
protected isDev: boolean,
protected filter: (level: LogLevelType) => boolean = null,
) {}
debug(message?: any, ...optionalParams: any[]) {
if (!this.isDev) {
return;
}
this.write(LogLevelType.Debug, message, ...optionalParams);
}
info(message?: any, ...optionalParams: any[]) {
this.write(LogLevelType.Info, message, ...optionalParams);
}
warning(message?: any, ...optionalParams: any[]) {
this.write(LogLevelType.Warning, message, ...optionalParams);
}
error(message?: any, ...optionalParams: any[]) {
this.write(LogLevelType.Error, message, ...optionalParams);
}
write(level: LogLevelType, message?: any, ...optionalParams: any[]) {
if (this.filter != null && this.filter(level)) {
return;
}
switch (level) {
case LogLevelType.Debug:
// eslint-disable-next-line
console.log(message, ...optionalParams);
break;
case LogLevelType.Info:
// eslint-disable-next-line
console.log(message, ...optionalParams);
break;
case LogLevelType.Warning:
// eslint-disable-next-line
console.warn(message, ...optionalParams);
break;
case LogLevelType.Error:
// eslint-disable-next-line
console.error(message, ...optionalParams);
break;
default:
break;
}
}
}
export { ConsoleLogService } from "@bitwarden/logging";

View File

@@ -9,7 +9,7 @@ describe("credential-id-utils", () => {
new Uint8Array([
0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07,
0xe7,
]),
]).buffer,
);
});
@@ -20,7 +20,7 @@ describe("credential-id-utils", () => {
new Uint8Array([
0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07,
0xe7,
]),
]).buffer,
);
});

View File

@@ -3,13 +3,13 @@
import { Fido2Utils } from "./fido2-utils";
import { guidToRawFormat } from "./guid-utils";
export function parseCredentialId(encodedCredentialId: string): Uint8Array {
export function parseCredentialId(encodedCredentialId: string): ArrayBuffer {
try {
if (encodedCredentialId.startsWith("b64.")) {
return Fido2Utils.stringToBuffer(encodedCredentialId.slice(4));
}
return guidToRawFormat(encodedCredentialId);
return guidToRawFormat(encodedCredentialId).buffer;
} catch {
return undefined;
}
@@ -18,13 +18,16 @@ export function parseCredentialId(encodedCredentialId: string): Uint8Array {
/**
* Compares two credential IDs for equality.
*/
export function compareCredentialIds(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) {
export function compareCredentialIds(a: ArrayBuffer, b: ArrayBuffer): boolean {
if (a.byteLength !== b.byteLength) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
const viewA = new Uint8Array(a);
const viewB = new Uint8Array(b);
for (let i = 0; i < viewA.length; i++) {
if (viewA[i] !== viewB[i]) {
return false;
}
}

View File

@@ -514,7 +514,7 @@ async function getPrivateKeyFromFido2Credential(
const keyBuffer = Fido2Utils.stringToBuffer(fido2Credential.keyValue);
return await crypto.subtle.importKey(
"pkcs8",
keyBuffer,
new Uint8Array(keyBuffer),
{
name: fido2Credential.keyAlgorithm,
namedCurve: fido2Credential.keyCurve,

View File

@@ -127,9 +127,9 @@ export class Fido2ClientService<ParentWindowReference>
}
const userId = Fido2Utils.stringToBuffer(params.user.id);
if (userId.length < 1 || userId.length > 64) {
if (userId.byteLength < 1 || userId.byteLength > 64) {
this.logService?.warning(
`[Fido2Client] Invalid 'user.id' length: ${params.user.id} (${userId.length})`,
`[Fido2Client] Invalid 'user.id' length: ${params.user.id} (${userId.byteLength})`,
);
throw new TypeError("Invalid 'user.id' length");
}

View File

@@ -47,8 +47,8 @@ export class Fido2Utils {
.replace(/=/g, "");
}
static stringToBuffer(str: string): Uint8Array {
return Fido2Utils.fromB64ToArray(Fido2Utils.fromUrlB64ToB64(str));
static stringToBuffer(str: string): ArrayBuffer {
return Fido2Utils.fromB64ToArray(Fido2Utils.fromUrlB64ToB64(str)).buffer;
}
static bufferSourceToUint8Array(bufferSource: BufferSource): Uint8Array {

View File

@@ -21,6 +21,7 @@ import {
BitwardenClient,
ClientSettings,
DeviceType as SdkDeviceType,
TokenProvider,
} from "@bitwarden/sdk-internal";
import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data";
@@ -41,6 +42,17 @@ import { EncryptedString } from "../../models/domain/enc-string";
// blocking the creation of an internal client for that user.
const UnsetClient = Symbol("UnsetClient");
/**
* A token provider that exposes the access token to the SDK.
*/
class JsTokenProvider implements TokenProvider {
constructor() {}
async get_access_token(): Promise<string | undefined> {
return undefined;
}
}
export class DefaultSdkService implements SdkService {
private sdkClientOverrides = new BehaviorSubject<{
[userId: UserId]: Rc<BitwardenClient> | typeof UnsetClient;
@@ -51,7 +63,7 @@ export class DefaultSdkService implements SdkService {
concatMap(async (env) => {
await SdkLoadService.Ready;
const settings = this.toSettings(env);
return await this.sdkClientFactory.createSdkClient(settings);
return await this.sdkClientFactory.createSdkClient(new JsTokenProvider(), settings);
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);
@@ -151,7 +163,10 @@ export class DefaultSdkService implements SdkService {
}
const settings = this.toSettings(env);
const client = await this.sdkClientFactory.createSdkClient(settings);
const client = await this.sdkClientFactory.createSdkClient(
new JsTokenProvider(),
settings,
);
await this.initializeClient(
userId,

View File

@@ -6,12 +6,13 @@ import { any, mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, map, of, timeout } from "rxjs";
import { Jsonify } from "type-fest";
import { StorageServiceProvider } from "@bitwarden/storage-core";
import { awaitAsync, trackEmissions } from "../../../../spec";
import { FakeStorageService } from "../../../../spec/fake-storage.service";
import { Account } from "../../../auth/abstractions/account.service";
import { UserId } from "../../../types/guid";
import { LogService } from "../../abstractions/log.service";
import { StorageServiceProvider } from "../../services/storage-service.provider";
import { StateDefinition } from "../state-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition } from "../user-key-definition";

View File

@@ -1,7 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { StorageServiceProvider } from "@bitwarden/storage-core";
import { LogService } from "../../abstractions/log.service";
import { StorageServiceProvider } from "../../services/storage-service.provider";
import { GlobalState } from "../global-state";
import { GlobalStateProvider } from "../global-state.provider";
import { KeyDefinition } from "../key-definition";

View File

@@ -1,8 +1,6 @@
import { AbstractStorageService, ObservableStorageService } from "@bitwarden/storage-core";
import { LogService } from "../../abstractions/log.service";
import {
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { GlobalState } from "../global-state";
import { KeyDefinition, globalKeyBuilder } from "../key-definition";

View File

@@ -1,8 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { StorageServiceProvider } from "@bitwarden/storage-core";
import { UserId } from "../../../types/guid";
import { LogService } from "../../abstractions/log.service";
import { StorageServiceProvider } from "../../services/storage-service.provider";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition } from "../user-key-definition";
import { SingleUserState } from "../user-state";

View File

@@ -1,11 +1,9 @@
import { Observable, combineLatest, of } from "rxjs";
import { AbstractStorageService, ObservableStorageService } from "@bitwarden/storage-core";
import { UserId } from "../../../types/guid";
import { LogService } from "../../abstractions/log.service";
import {
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition } from "../user-key-definition";
import { CombinedState, SingleUserState } from "../user-state";

View File

@@ -1,10 +1,11 @@
import { mock } from "jest-mock-extended";
import { StorageServiceProvider } from "@bitwarden/storage-core";
import { mockAccountServiceWith } from "../../../../spec/fake-account-service";
import { FakeStorageService } from "../../../../spec/fake-storage.service";
import { UserId } from "../../../types/guid";
import { LogService } from "../../abstractions/log.service";
import { StorageServiceProvider } from "../../services/storage-service.provider";
import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";

View File

@@ -15,12 +15,10 @@ import {
} from "rxjs";
import { Jsonify } from "type-fest";
import { AbstractStorageService, ObservableStorageService } from "@bitwarden/storage-core";
import { StorageKey } from "../../../types/state";
import { LogService } from "../../abstractions/log.service";
import {
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { DebugOptions } from "../key-definition";
import { populateOptionsWithDefault, StateUpdateOptions } from "../state-update-options";

View File

@@ -1,6 +1,6 @@
import { Jsonify } from "type-fest";
import { AbstractStorageService } from "../../abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/storage-core";
export async function getStoredValue<T>(
key: string,

View File

@@ -1,8 +1,12 @@
import { mock } from "jest-mock-extended";
import {
AbstractStorageService,
ObservableStorageService,
StorageServiceProvider,
} from "@bitwarden/storage-core";
import { FakeGlobalStateProvider } from "../../../spec";
import { AbstractStorageService, ObservableStorageService } from "../abstractions/storage.service";
import { StorageServiceProvider } from "../services/storage-service.provider";
import { StateDefinition } from "./state-definition";
import { STATE_LOCK_EVENT, StateEventRegistrarService } from "./state-event-registrar.service";

View File

@@ -1,9 +1,13 @@
import { mock } from "jest-mock-extended";
import {
AbstractStorageService,
ObservableStorageService,
StorageServiceProvider,
} from "@bitwarden/storage-core";
import { FakeGlobalStateProvider } from "../../../spec";
import { UserId } from "../../types/guid";
import { AbstractStorageService, ObservableStorageService } from "../abstractions/storage.service";
import { StorageServiceProvider } from "../services/storage-service.provider";
import { STATE_LOCK_EVENT } from "./state-event-registrar.service";
import { StateEventRunnerService } from "./state-event-runner.service";

View File

@@ -2,8 +2,9 @@
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
import { StorageServiceProvider } from "@bitwarden/storage-core";
import { UserId } from "../../types/guid";
import { StorageServiceProvider } from "../services/storage-service.provider";
import { GlobalState } from "./global-state";
import { GlobalStateProvider } from "./global-state.provider";

View File

@@ -97,7 +97,7 @@ import { PaymentResponse } from "../billing/models/response/payment.response";
import { PlanResponse } from "../billing/models/response/plan.response";
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
import { DeviceType } from "../enums";
import { ClientType, DeviceType } from "../enums";
import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request";
import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request";
import { VaultTimeoutSettingsService } from "../key-management/vault-timeout";
@@ -154,8 +154,6 @@ export type HttpOperations = {
export class ApiService implements ApiServiceAbstraction {
private device: DeviceType;
private deviceType: string;
private isWebClient = false;
private isDesktopClient = false;
private refreshTokenPromise: Promise<string> | undefined;
/**
@@ -178,22 +176,6 @@ export class ApiService implements ApiServiceAbstraction {
) {
this.device = platformUtilsService.getDevice();
this.deviceType = this.device.toString();
this.isWebClient =
this.device === DeviceType.IEBrowser ||
this.device === DeviceType.ChromeBrowser ||
this.device === DeviceType.EdgeBrowser ||
this.device === DeviceType.FirefoxBrowser ||
this.device === DeviceType.OperaBrowser ||
this.device === DeviceType.SafariBrowser ||
this.device === DeviceType.UnknownBrowser ||
this.device === DeviceType.VivaldiBrowser;
this.isDesktopClient =
this.device === DeviceType.WindowsDesktop ||
this.device === DeviceType.MacOsDesktop ||
this.device === DeviceType.LinuxDesktop ||
this.device === DeviceType.WindowsCLI ||
this.device === DeviceType.MacOsCLI ||
this.device === DeviceType.LinuxCLI;
}
// Auth APIs
@@ -838,7 +820,9 @@ export class ApiService implements ApiServiceAbstraction {
// Sync APIs
async getSync(): Promise<SyncResponse> {
const path = this.isDesktopClient || this.isWebClient ? "/sync?excludeDomains=true" : "/sync";
const path = !this.platformUtilsService.supportsAutofill()
? "/sync?excludeDomains=true"
: "/sync";
const r = await this.send("GET", path, null, true, true);
return new SyncResponse(r);
}
@@ -1875,7 +1859,7 @@ export class ApiService implements ApiServiceAbstraction {
private async getCredentials(): Promise<RequestCredentials> {
const env = await firstValueFrom(this.environmentService.environment$);
if (!this.isWebClient || env.hasBaseUrl()) {
if (this.platformUtilsService.getClientType() !== ClientType.Web || env.hasBaseUrl()) {
return "include";
}
return undefined;

View File

@@ -2,7 +2,9 @@ import { Opaque } from "type-fest";
export type Guid = Opaque<string, "Guid">;
export type UserId = Opaque<string, "UserId">;
// Convenience re-export of UserId from it's original location, any library that
// wants to be lower level than common should instead import it from user-core.
export { UserId } from "@bitwarden/user-core";
export type OrganizationId = Opaque<string, "OrganizationId">;
export type CollectionId = Opaque<string, "CollectionId">;
export type ProviderId = Opaque<string, "ProviderId">;

View File

@@ -353,14 +353,14 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
type: this.type,
favorite: this.favorite ?? false,
organizationUseTotp: this.organizationUseTotp ?? false,
edit: this.edit,
edit: this.edit ?? true,
permissions: this.permissions
? {
delete: this.permissions.delete,
restore: this.permissions.restore,
}
: undefined,
viewPassword: this.viewPassword,
viewPassword: this.viewPassword ?? true,
localData: this.localData
? {
lastUsedDate: this.localData.lastUsedDate

View File

@@ -26,6 +26,8 @@ describe("AttachmentView", () => {
});
it("should return an AttachmentView from an SdkAttachmentView", () => {
jest.spyOn(SymmetricCryptoKey, "fromString").mockReturnValue("mockKey" as any);
const sdkAttachmentView = {
id: "id",
url: "url",
@@ -33,6 +35,7 @@ describe("AttachmentView", () => {
sizeName: "sizeName",
fileName: "fileName",
key: "encKeyB64_fromString",
decryptedKey: "decryptedKey_B64",
} as SdkAttachmentView;
const result = AttachmentView.fromSdkAttachmentView(sdkAttachmentView);
@@ -43,14 +46,20 @@ describe("AttachmentView", () => {
size: "size",
sizeName: "sizeName",
fileName: "fileName",
key: null,
key: "mockKey",
encryptedKey: new EncString(sdkAttachmentView.key as string),
});
expect(SymmetricCryptoKey.fromString).toHaveBeenCalledWith("decryptedKey_B64");
});
});
describe("toSdkAttachmentView", () => {
it("should convert AttachmentView to SdkAttachmentView", () => {
const mockKey = {
toBase64: jest.fn().mockReturnValue("keyB64"),
} as any;
const attachmentView = new AttachmentView();
attachmentView.id = "id";
attachmentView.url = "url";
@@ -58,8 +67,10 @@ describe("AttachmentView", () => {
attachmentView.sizeName = "sizeName";
attachmentView.fileName = "fileName";
attachmentView.encryptedKey = new EncString("encKeyB64");
attachmentView.key = mockKey;
const result = attachmentView.toSdkAttachmentView();
expect(result).toEqual({
id: "id",
url: "url",
@@ -67,6 +78,7 @@ describe("AttachmentView", () => {
sizeName: "sizeName",
fileName: "fileName",
key: "encKeyB64",
decryptedKey: "keyB64",
});
});
});

View File

@@ -59,6 +59,8 @@ export class AttachmentView implements View {
sizeName: this.sizeName,
fileName: this.fileName,
key: this.encryptedKey?.toJSON(),
// TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete
decryptedKey: this.key ? this.key.toBase64() : null,
};
}
@@ -76,6 +78,8 @@ export class AttachmentView implements View {
view.size = obj.size ?? null;
view.sizeName = obj.sizeName ?? null;
view.fileName = obj.fileName ?? null;
// TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete
view.key = obj.key ? SymmetricCryptoKey.fromString(obj.decryptedKey) : null;
view.encryptedKey = new EncString(obj.key);
return view;

View File

@@ -383,7 +383,7 @@ export class CipherService implements CipherServiceAbstraction {
const decCiphers = await this.getDecryptedCiphers(userId);
if (decCiphers != null && decCiphers.length !== 0) {
await this.reindexCiphers(userId);
return await this.getDecryptedCiphers(userId);
return decCiphers;
}
const decrypted = await this.decryptCiphers(await this.getAll(userId), userId);

View File

@@ -7,7 +7,7 @@ import {
CipherType as SdkCipherType,
CipherView as SdkCipherView,
CipherListView,
Attachment as SdkAttachment,
AttachmentView as SdkAttachmentView,
} from "@bitwarden/sdk-internal";
import { mockEnc } from "../../../spec";
@@ -311,7 +311,9 @@ describe("DefaultCipherEncryptionService", () => {
const expectedDecryptedContent = new Uint8Array([5, 6, 7, 8]);
jest.spyOn(cipher, "toSdkCipher").mockReturnValue({ id: "id" } as SdkCipher);
jest.spyOn(attachment, "toSdkAttachmentView").mockReturnValue({ id: "a1" } as SdkAttachment);
jest
.spyOn(attachment, "toSdkAttachmentView")
.mockReturnValue({ id: "a1" } as SdkAttachmentView);
mockSdkClient.vault().attachments().decrypt_buffer.mockReturnValue(expectedDecryptedContent);
const result = await cipherEncryptionService.decryptAttachmentContent(

View File

@@ -91,7 +91,6 @@ export class RestrictedItemTypesService {
* Restriction logic:
* - If cipher type is not restricted by any org → allowed
* - If cipher belongs to an org that allows this type → allowed
* - If cipher is personal vault and any org allows this type → allowed
* - Otherwise → restricted
*/
isCipherRestricted(cipher: CipherLike, restrictedTypes: RestrictedCipherType[]): boolean {
@@ -108,8 +107,8 @@ export class RestrictedItemTypesService {
return !restriction.allowViewOrgIds.includes(cipher.organizationId);
}
// For personal vault ciphers: restricted only if NO organizations allow this type
return restriction.allowViewOrgIds.length === 0;
// Cipher is restricted by at least one organization, restrict it
return true;
}
/**

View File

@@ -365,7 +365,7 @@ describe("Default task service", () => {
const subscription = service.listenForTaskNotifications();
const notification = {
type: NotificationType.PendingSecurityTasks,
type: NotificationType.RefreshSecurityTasks,
} as NotificationResponse;
mockNotifications$.next([notification, userId]);
@@ -390,7 +390,7 @@ describe("Default task service", () => {
const subscription = service.listenForTaskNotifications();
const notification = {
type: NotificationType.PendingSecurityTasks,
type: NotificationType.RefreshSecurityTasks,
} as NotificationResponse;
mockNotifications$.next([notification, "other-user-id" as UserId]);

View File

@@ -152,7 +152,7 @@ export class DefaultTaskService implements TaskService {
return this.notificationService.notifications$.pipe(
filter(
([notification, userId]) =>
notification.type === NotificationType.PendingSecurityTasks &&
notification.type === NotificationType.RefreshSecurityTasks &&
filterByUserIds.includes(userId),
),
map(([, userId]) => userId),

View File

@@ -0,0 +1,22 @@
import { DeviceType } from "@bitwarden/common/enums";
/**
* Returns the web store URL for the Bitwarden browser extension based on the device type.
* @defaults Bitwarden download page
*/
export const getWebStoreUrl = (deviceType: DeviceType): string => {
switch (deviceType) {
case DeviceType.ChromeBrowser:
return "https://chromewebstore.google.com/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb";
case DeviceType.FirefoxBrowser:
return "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/";
case DeviceType.SafariBrowser:
return "https://apps.apple.com/us/app/bitwarden/id1352778147?mt=12";
case DeviceType.OperaBrowser:
return "https://addons.opera.com/extensions/details/bitwarden-free-password-manager/";
case DeviceType.EdgeBrowser:
return "https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh";
default:
return "https://bitwarden.com/download/#downloads-web-browser";
}
};