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

Merge branch 'main' into km/replace-encstring-with-unsigned-shared-key

This commit is contained in:
Bernd Schoolmann
2025-12-12 18:34:10 +01:00
committed by GitHub
330 changed files with 8886 additions and 4437 deletions

View File

@@ -6,19 +6,26 @@ import { ReplaySubject, combineLatest, map, Observable } from "rxjs";
import { Account, AccountInfo, AccountService } from "../src/auth/abstractions/account.service";
import { UserId } from "../src/types/guid";
/**
* Creates a mock AccountInfo object with sensible defaults that can be overridden.
* Use this when you need just an AccountInfo object in tests.
*/
export function mockAccountInfoWith(info: Partial<AccountInfo> = {}): AccountInfo {
return {
name: "name",
email: "email",
emailVerified: true,
creationDate: "2024-01-01T00:00:00.000Z",
...info,
};
}
export function mockAccountServiceWith(
userId: UserId,
info: Partial<AccountInfo> = {},
activity: Record<UserId, Date> = {},
): FakeAccountService {
const fullInfo: AccountInfo = {
...info,
...{
name: "name",
email: "email",
emailVerified: true,
},
};
const fullInfo = mockAccountInfoWith(info);
const fullActivity = { [userId]: new Date(), ...activity };
@@ -104,6 +111,10 @@ export class FakeAccountService implements AccountService {
await this.mock.setAccountEmailVerified(userId, emailVerified);
}
async setAccountCreationDate(userId: UserId, creationDate: string): Promise<void> {
await this.mock.setAccountCreationDate(userId, creationDate);
}
async switchAccount(userId: UserId): Promise<void> {
const next =
userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] };
@@ -127,4 +138,5 @@ const loggedOutInfo: AccountInfo = {
name: undefined,
email: "",
emailVerified: false,
creationDate: undefined,
};

View File

@@ -50,6 +50,7 @@ import { UpdateProfileRequest } from "../auth/models/request/update-profile.requ
import { ApiKeyResponse } from "../auth/models/response/api-key.response";
import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response";
import { IdentitySsoRequiredResponse } from "../auth/models/response/identity-sso-required.response";
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
@@ -140,7 +141,10 @@ export abstract class ApiService {
| UserApiTokenRequest
| WebAuthnLoginTokenRequest,
): Promise<
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
| IdentityTokenResponse
| IdentityTwoFactorResponse
| IdentityDeviceVerificationResponse
| IdentitySsoRequiredResponse
>;
abstract refreshIdentityToken(userId?: UserId): Promise<any>;

View File

@@ -11,6 +11,7 @@ export class PolicyData {
type: PolicyType;
data: Record<string, string | number | boolean>;
enabled: boolean;
revisionDate: string;
constructor(response?: PolicyResponse) {
if (response == null) {
@@ -22,6 +23,7 @@ export class PolicyData {
this.type = response.type;
this.data = response.data;
this.enabled = response.enabled;
this.revisionDate = response.revisionDate;
}
static fromPolicy(policy: Policy): PolicyData {

View File

@@ -19,6 +19,8 @@ export class Policy extends Domain {
*/
enabled: boolean;
revisionDate: Date;
constructor(obj?: PolicyData) {
super();
if (obj == null) {
@@ -30,6 +32,7 @@ export class Policy extends Domain {
this.type = obj.type;
this.data = obj.data;
this.enabled = obj.enabled;
this.revisionDate = new Date(obj.revisionDate);
}
static fromResponse(response: PolicyResponse): Policy {

View File

@@ -9,6 +9,7 @@ export class PolicyResponse extends BaseResponse {
data: any;
enabled: boolean;
canToggleState: boolean;
revisionDate: string;
constructor(response: any) {
super(response);
@@ -18,5 +19,6 @@ export class PolicyResponse extends BaseResponse {
this.data = this.getResponseProperty("Data");
this.enabled = this.getResponseProperty("Enabled");
this.canToggleState = this.getResponseProperty("CanToggleState") ?? true;
this.revisionDate = this.getResponseProperty("RevisionDate");
}
}

View File

@@ -27,9 +27,7 @@ function buildKeyDefinition<T>(key: string): UserKeyDefinition<T> {
export const AUTO_CONFIRM_FINGERPRINTS = buildKeyDefinition<boolean>("autoConfirmFingerPrints");
export class DefaultOrganizationManagementPreferencesService
implements OrganizationManagementPreferencesService
{
export class DefaultOrganizationManagementPreferencesService implements OrganizationManagementPreferencesService {
constructor(private stateProvider: StateProvider) {}
autoConfirmFingerPrints = this.buildOrganizationManagementPreference(

View File

@@ -83,12 +83,15 @@ describe("PolicyService", () => {
type: PolicyType.MaximumVaultTimeout,
enabled: true,
data: { minutes: 14 },
revisionDate: expect.any(Date),
},
{
id: "99",
organizationId: "test-organization",
type: PolicyType.DisableSend,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
]);
});
@@ -113,6 +116,8 @@ describe("PolicyService", () => {
organizationId: "test-organization",
type: PolicyType.DisableSend,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
]);
});
@@ -242,6 +247,8 @@ describe("PolicyService", () => {
organizationId: "org1",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
});
});
@@ -331,24 +338,32 @@ describe("PolicyService", () => {
organizationId: "org4",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
{
id: "policy2",
organizationId: "org1",
type: PolicyType.ActivateAutofill,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
{
id: "policy3",
organizationId: "org5",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
{
id: "policy4",
organizationId: "org1",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
]);
});
@@ -371,24 +386,32 @@ describe("PolicyService", () => {
organizationId: "org4",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
{
id: "policy2",
organizationId: "org1",
type: PolicyType.ActivateAutofill,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
{
id: "policy3",
organizationId: "org5",
type: PolicyType.DisablePersonalVaultExport,
enabled: false,
data: undefined,
revisionDate: expect.any(Date),
},
{
id: "policy4",
organizationId: "org1",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
]);
});
@@ -411,24 +434,32 @@ describe("PolicyService", () => {
organizationId: "org4",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
{
id: "policy2",
organizationId: "org1",
type: PolicyType.ActivateAutofill,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
{
id: "policy3",
organizationId: "org5",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
{
id: "policy4",
organizationId: "org2",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
]);
});
@@ -451,24 +482,32 @@ describe("PolicyService", () => {
organizationId: "org4",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
{
id: "policy2",
organizationId: "org1",
type: PolicyType.ActivateAutofill,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
{
id: "policy3",
organizationId: "org3",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
{
id: "policy4",
organizationId: "org1",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
data: undefined,
revisionDate: expect.any(Date),
},
]);
});
@@ -788,6 +827,7 @@ describe("PolicyService", () => {
policyData.type = type;
policyData.enabled = enabled;
policyData.data = data;
policyData.revisionDate = new Date().toISOString();
return policyData;
}

View File

@@ -2,14 +2,11 @@ import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
/**
* Holds information about an account for use in the AccountService
* if more information is added, be sure to update the equality method.
*/
export type AccountInfo = {
email: string;
emailVerified: boolean;
name: string | undefined;
creationDate: string | undefined;
};
export type Account = { id: UserId } & AccountInfo;
@@ -75,6 +72,12 @@ export abstract class AccountService {
* @param emailVerified
*/
abstract setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise<void>;
/**
* updates the `accounts$` observable with the creation date for the account.
* @param userId
* @param creationDate
*/
abstract setAccountCreationDate(userId: UserId, creationDate: string): Promise<void>;
/**
* updates the `accounts$` observable with the new VerifyNewDeviceLogin property for the account.
* @param userId

View File

@@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "../../../types/guid";
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
@@ -18,10 +20,16 @@ export class AuthResult {
email: string;
requiresEncryptionKeyMigration: boolean;
requiresDeviceVerification: boolean;
ssoOrganizationIdentifier?: string | null;
// The master-password used in the authentication process
masterPassword: string | null;
get requiresTwoFactor() {
return this.twoFactorProviders != null;
}
// This is not as extensible as an object-based approach. In the future we may need to adjust to an object based approach.
get requiresSso() {
return !Utils.isNullOrWhitespace(this.ssoOrganizationIdentifier);
}
}

View File

@@ -0,0 +1,10 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
export class IdentitySsoRequiredResponse extends BaseResponse {
ssoOrganizationIdentifier: string | null;
constructor(response: any) {
super(response);
this.ssoOrganizationIdentifier = this.getResponseProperty("SsoOrganizationIdentifier");
}
}

View File

@@ -6,6 +6,7 @@
import { MockProxy, mock } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { mockAccountInfoWith } from "../../../spec/fake-account-service";
import { FakeGlobalState } from "../../../spec/fake-state";
import {
FakeGlobalStateProvider,
@@ -27,7 +28,7 @@ import {
} from "./account.service";
describe("accountInfoEqual", () => {
const accountInfo: AccountInfo = { name: "name", email: "email", emailVerified: true };
const accountInfo = mockAccountInfoWith();
it("compares nulls", () => {
expect(accountInfoEqual(null, null)).toBe(true);
@@ -64,6 +65,23 @@ describe("accountInfoEqual", () => {
expect(accountInfoEqual(accountInfo, same)).toBe(true);
expect(accountInfoEqual(accountInfo, different)).toBe(false);
});
it("compares creationDate", () => {
const same = { ...accountInfo };
const different = { ...accountInfo, creationDate: "2024-12-31T00:00:00.000Z" };
expect(accountInfoEqual(accountInfo, same)).toBe(true);
expect(accountInfoEqual(accountInfo, different)).toBe(false);
});
it("compares undefined creationDate", () => {
const accountWithoutCreationDate = mockAccountInfoWith({ creationDate: undefined });
const same = { ...accountWithoutCreationDate };
const different = { ...accountWithoutCreationDate, creationDate: "2024-01-01T00:00:00.000Z" };
expect(accountInfoEqual(accountWithoutCreationDate, same)).toBe(true);
expect(accountInfoEqual(accountWithoutCreationDate, different)).toBe(false);
});
});
describe("accountService", () => {
@@ -76,7 +94,10 @@ describe("accountService", () => {
let activeAccountIdState: FakeGlobalState<UserId>;
let accountActivityState: FakeGlobalState<Record<UserId, Date>>;
const userId = Utils.newGuid() as UserId;
const userInfo = { email: "email", name: "name", emailVerified: true };
const userInfo = mockAccountInfoWith({
email: "email",
name: "name",
});
beforeEach(() => {
messagingService = mock();
@@ -253,6 +274,56 @@ describe("accountService", () => {
});
});
describe("setCreationDate", () => {
const initialState = { [userId]: userInfo };
beforeEach(() => {
accountsState.stateSubject.next(initialState);
});
it("should update the account with a new creation date", async () => {
const newCreationDate = "2024-12-31T00:00:00.000Z";
await sut.setAccountCreationDate(userId, newCreationDate);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({
[userId]: { ...userInfo, creationDate: newCreationDate },
});
});
it("should not update if the creation date is the same", async () => {
await sut.setAccountCreationDate(userId, userInfo.creationDate);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual(initialState);
});
it("should update from undefined to a defined creation date", async () => {
const accountWithoutCreationDate = mockAccountInfoWith({
...userInfo,
creationDate: undefined,
});
accountsState.stateSubject.next({ [userId]: accountWithoutCreationDate });
const newCreationDate = "2024-06-15T12:30:00.000Z";
await sut.setAccountCreationDate(userId, newCreationDate);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({
[userId]: { ...accountWithoutCreationDate, creationDate: newCreationDate },
});
});
it("should update to a different creation date string format", async () => {
const newCreationDate = "2023-03-15T08:45:30.123Z";
await sut.setAccountCreationDate(userId, newCreationDate);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({
[userId]: { ...userInfo, creationDate: newCreationDate },
});
});
});
describe("setAccountVerifyNewDeviceLogin", () => {
const initialState = true;
beforeEach(() => {
@@ -294,6 +365,7 @@ describe("accountService", () => {
email: "",
emailVerified: false,
name: undefined,
creationDate: undefined,
},
});
});

View File

@@ -62,6 +62,7 @@ const LOGGED_OUT_INFO: AccountInfo = {
email: "",
emailVerified: false,
name: undefined,
creationDate: undefined,
};
/**
@@ -167,6 +168,10 @@ export class AccountServiceImplementation implements InternalAccountService {
await this.setAccountInfo(userId, { emailVerified });
}
async setAccountCreationDate(userId: UserId, creationDate: string): Promise<void> {
await this.setAccountInfo(userId, { creationDate });
}
async clean(userId: UserId) {
await this.setAccountInfo(userId, LOGGED_OUT_INFO);
await this.removeAccountActivity(userId);

View File

@@ -15,6 +15,7 @@ import {
SystemNotificationEvent,
SystemNotificationsService,
} from "@bitwarden/common/platform/system-notifications/system-notifications.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/user-core";
import { AuthRequestAnsweringService } from "./auth-request-answering.service";
@@ -48,14 +49,16 @@ describe("AuthRequestAnsweringService", () => {
// Common defaults
authService.activeAccountStatus$ = of(AuthenticationStatus.Locked);
accountService.activeAccount$ = of({
id: userId,
const accountInfo = mockAccountInfoWith({
email: "user@example.com",
emailVerified: true,
name: "User",
});
accountService.activeAccount$ = of({
id: userId,
...accountInfo,
});
accountService.accounts$ = of({
[userId]: { email: "user@example.com", emailVerified: true, name: "User" },
[userId]: accountInfo,
});
(masterPasswordService.forceSetPasswordReason$ as jest.Mock).mockReturnValue(
of(ForceSetPasswordReason.None),

View File

@@ -10,6 +10,7 @@ import {
makeStaticByteArray,
mockAccountServiceWith,
trackEmissions,
mockAccountInfoWith,
} from "../../../spec";
import { ApiService } from "../../abstractions/api.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
@@ -58,9 +59,10 @@ describe("AuthService", () => {
const accountInfo = {
status: AuthenticationStatus.Unlocked,
id: userId,
email: "email",
emailVerified: false,
name: "name",
...mockAccountInfoWith({
email: "email",
name: "name",
}),
};
beforeEach(() => {
@@ -112,9 +114,10 @@ describe("AuthService", () => {
const accountInfo2 = {
status: AuthenticationStatus.Unlocked,
id: Utils.newGuid() as UserId,
email: "email2",
emailVerified: false,
name: "name2",
...mockAccountInfoWith({
email: "email2",
name: "name2",
}),
};
const emissions = trackEmissions(sut.activeAccountStatus$);
@@ -131,11 +134,13 @@ describe("AuthService", () => {
it("requests auth status for all known users", async () => {
const userId2 = Utils.newGuid() as UserId;
await accountService.addAccount(userId2, {
email: "email2",
emailVerified: false,
name: "name2",
});
await accountService.addAccount(
userId2,
mockAccountInfoWith({
email: "email2",
name: "name2",
}),
);
const mockFn = jest.fn().mockReturnValue(of(AuthenticationStatus.Locked));
sut.authStatusFor$ = mockFn;

View File

@@ -8,12 +8,13 @@ import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "@bitwarden/key-management";
import { mockAccountInfoWith } from "../../../spec/fake-account-service";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { UserId } from "../../types/guid";
import { Account, AccountInfo, AccountService } from "../abstractions/account.service";
import { Account, AccountService } from "../abstractions/account.service";
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
@@ -96,11 +97,10 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
const encryptedKey = "encryptedString";
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
const user1AccountInfo: AccountInfo = {
const user1AccountInfo = mockAccountInfoWith({
name: "Test User 1",
email: "test1@email.com",
emailVerified: true,
};
});
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
keyService.userKey$.mockReturnValue(of({ key: "key" } as any));

View File

@@ -22,9 +22,7 @@ import { UserKey } from "../../types/key";
import { AccountService } from "../abstractions/account.service";
import { PasswordResetEnrollmentServiceAbstraction } from "../abstractions/password-reset-enrollment.service.abstraction";
export class PasswordResetEnrollmentServiceImplementation
implements PasswordResetEnrollmentServiceAbstraction
{
export class PasswordResetEnrollmentServiceImplementation implements PasswordResetEnrollmentServiceAbstraction {
constructor(
protected organizationApiService: OrganizationApiServiceAbstraction,
protected accountService: AccountService,

View File

@@ -445,13 +445,15 @@ export class TokenService implements TokenServiceAbstraction {
// we can't determine storage location w/out vaultTimeoutAction and vaultTimeout
// but we can simply clear all locations to avoid the need to require those parameters.
// When secure storage is supported, clear the encryption key from secure storage.
// When not supported (e.g., portable builds), tokens are stored on disk and this step is skipped.
if (this.platformSupportsSecureStorage) {
// Always clear the access token key when clearing the access token
// The next set of the access token will create a new access token key
// Always clear the access token key when clearing the access token.
// The next set of the access token will create a new access token key.
await this.clearAccessTokenKey(userId);
}
// Platform doesn't support secure storage, so use state provider implementation
// Clear tokens from disk storage (all platforms)
await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_DISK).update((_) => null, {
shouldUpdate: (previousValue) => previousValue !== null,
});
@@ -478,6 +480,9 @@ export class TokenService implements TokenServiceAbstraction {
return null;
}
// When platformSupportsSecureStorage=true, tokens on disk are encrypted and require
// decryption keys from secure storage. When false (e.g., portable builds), tokens are
// stored on disk.
if (this.platformSupportsSecureStorage) {
let accessTokenKey: AccessTokenKey;
try {
@@ -1118,6 +1123,9 @@ export class TokenService implements TokenServiceAbstraction {
) {
return TokenStorageLocation.Memory;
} else {
// Secure storage (e.g., OS credential manager) is preferred when available.
// Desktop portable builds set platformSupportsSecureStorage=false to store tokens
// on disk for portability across machines.
if (useSecureStorage && this.platformSupportsSecureStorage) {
return TokenStorageLocation.SecureStorage;
}

View File

@@ -4,9 +4,7 @@ import { PlatformUtilsService } from "../../../platform/abstractions/platform-ut
import { OrganizationSponsorshipApiServiceAbstraction } from "../../abstractions/organizations/organization-sponsorship-api.service.abstraction";
import { OrganizationSponsorshipInvitesResponse } from "../../models/response/organization-sponsorship-invites.response";
export class OrganizationSponsorshipApiService
implements OrganizationSponsorshipApiServiceAbstraction
{
export class OrganizationSponsorshipApiService implements OrganizationSponsorshipApiServiceAbstraction {
constructor(
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,

View File

@@ -43,6 +43,7 @@ export enum FeatureFlag {
LinuxBiometricsV2 = "pm-26340-linux-biometrics-v2",
UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data",
NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change",
DataRecoveryTool = "pm-28813-data-recovery-tool",
ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component",
/* Tools */
@@ -64,6 +65,7 @@ export enum FeatureFlag {
RiskInsightsForPremium = "pm-23904-risk-insights-for-premium",
VaultLoadingSkeletons = "pm-25081-vault-skeleton-loaders",
BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight",
MigrateMyVaultToMyItems = "pm-20558-migrate-myvault-to-myitems",
/* Platform */
IpcChannelFramework = "ipc-channel-framework",
@@ -123,6 +125,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.RiskInsightsForPremium]: FALSE,
[FeatureFlag.VaultLoadingSkeletons]: FALSE,
[FeatureFlag.BrowserPremiumSpotlight]: FALSE,
[FeatureFlag.MigrateMyVaultToMyItems]: FALSE,
/* Auth */
[FeatureFlag.PM23801_PrefetchPasswordPrelogin]: FALSE,
@@ -147,6 +150,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.LinuxBiometricsV2]: FALSE,
[FeatureFlag.UnlockWithMasterPasswordUnlockData]: FALSE,
[FeatureFlag.NoLogoutOnKdfChange]: FALSE,
[FeatureFlag.DataRecoveryTool]: FALSE,
[FeatureFlag.ConsolidatedSessionTimeoutComponent]: FALSE,
/* Platform */

View File

@@ -91,4 +91,12 @@ export class DefaultKeyGenerationService implements KeyGenerationService {
return new SymmetricCryptoKey(newKey);
}
async deriveVaultExportKey(
password: string,
salt: string,
kdfConfig: KdfConfig,
): Promise<SymmetricCryptoKey> {
return await this.stretchKey(await this.deriveKeyFromPassword(password, salt, kdfConfig));
}
}

View File

@@ -87,4 +87,19 @@ export abstract class KeyGenerationService {
* @returns 64 byte derived key.
*/
abstract stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey>;
/**
* Derives a 64 byte key for encrypting and decrypting vault exports.
*
* @deprecated Do not use this for new use-cases.
* @param password Password to derive the key from.
* @param salt Salt for the key derivation function.
* @param kdfConfig Configuration for the key derivation function.
* @returns 64 byte derived key.
*/
abstract deriveVaultExportKey(
password: string,
salt: string,
kdfConfig: KdfConfig,
): Promise<SymmetricCryptoKey>;
}

View File

@@ -0,0 +1,7 @@
import { KeyConnectorConfirmationDetailsResponse } from "../models/response/key-connector-confirmation-details.response";
export abstract class KeyConnectorApiService {
abstract getConfirmationDetails(
orgSsoIdentifier: string,
): Promise<KeyConnectorConfirmationDetailsResponse>;
}

View File

@@ -1,3 +1,4 @@
export interface KeyConnectorDomainConfirmation {
keyConnectorUrl: string;
organizationSsoIdentifier: string;
}

View File

@@ -0,0 +1,10 @@
import { BaseResponse } from "../../../../models/response/base.response";
export class KeyConnectorConfirmationDetailsResponse extends BaseResponse {
organizationName: string;
constructor(response: any) {
super(response);
this.organizationName = this.getResponseProperty("OrganizationName");
}
}

View File

@@ -0,0 +1,54 @@
import { mock, MockProxy } from "jest-mock-extended";
import { ApiService } from "../../../abstractions/api.service";
import { KeyConnectorConfirmationDetailsResponse } from "../models/response/key-connector-confirmation-details.response";
import { DefaultKeyConnectorApiService } from "./default-key-connector-api.service";
describe("DefaultKeyConnectorApiService", () => {
let apiService: MockProxy<ApiService>;
let sut: DefaultKeyConnectorApiService;
beforeEach(() => {
apiService = mock<ApiService>();
sut = new DefaultKeyConnectorApiService(apiService);
});
describe("getConfirmationDetails", () => {
it("encodes orgSsoIdentifier in URL", async () => {
const orgSsoIdentifier = "test org/with special@chars";
const expectedEncodedIdentifier = encodeURIComponent(orgSsoIdentifier);
const mockResponse = {};
apiService.send.mockResolvedValue(mockResponse);
await sut.getConfirmationDetails(orgSsoIdentifier);
expect(apiService.send).toHaveBeenCalledWith(
"GET",
`/accounts/key-connector/confirmation-details/${expectedEncodedIdentifier}`,
null,
true,
true,
);
});
it("returns expected response", async () => {
const orgSsoIdentifier = "test-org";
const expectedOrgName = "example";
const mockResponse = { OrganizationName: expectedOrgName };
apiService.send.mockResolvedValue(mockResponse);
const result = await sut.getConfirmationDetails(orgSsoIdentifier);
expect(result).toBeInstanceOf(KeyConnectorConfirmationDetailsResponse);
expect(result.organizationName).toBe(expectedOrgName);
expect(apiService.send).toHaveBeenCalledWith(
"GET",
"/accounts/key-connector/confirmation-details/test-org",
null,
true,
true,
);
});
});
});

View File

@@ -0,0 +1,20 @@
import { ApiService } from "../../../abstractions/api.service";
import { KeyConnectorApiService } from "../abstractions/key-connector-api.service";
import { KeyConnectorConfirmationDetailsResponse } from "../models/response/key-connector-confirmation-details.response";
export class DefaultKeyConnectorApiService implements KeyConnectorApiService {
constructor(private apiService: ApiService) {}
async getConfirmationDetails(
orgSsoIdentifier: string,
): Promise<KeyConnectorConfirmationDetailsResponse> {
const r = await this.apiService.send(
"GET",
"/accounts/key-connector/confirmation-details/" + encodeURIComponent(orgSsoIdentifier),
null,
true,
true,
);
return new KeyConnectorConfirmationDetailsResponse(r);
}
}

View File

@@ -603,7 +603,10 @@ describe("KeyConnectorService", () => {
const data$ = keyConnectorService.requiresDomainConfirmation$(mockUserId);
const data = await firstValueFrom(data$);
expect(data).toEqual({ keyConnectorUrl: conversion.keyConnectorUrl });
expect(data).toEqual({
keyConnectorUrl: conversion.keyConnectorUrl,
organizationSsoIdentifier: conversion.organizationId,
});
});
it("should return observable of null value when no data is set", async () => {

View File

@@ -202,9 +202,16 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
}
requiresDomainConfirmation$(userId: UserId): Observable<KeyConnectorDomainConfirmation | null> {
return this.stateProvider
.getUserState$(NEW_SSO_USER_KEY_CONNECTOR_CONVERSION, userId)
.pipe(map((data) => (data != null ? { keyConnectorUrl: data.keyConnectorUrl } : null)));
return this.stateProvider.getUserState$(NEW_SSO_USER_KEY_CONNECTOR_CONVERSION, userId).pipe(
map((data) =>
data != null
? {
keyConnectorUrl: data.keyConnectorUrl,
organizationSsoIdentifier: data.organizationId,
}
: null,
),
);
}
private handleKeyConnectorError(e: any) {

View File

@@ -45,14 +45,6 @@ export abstract class PinStateServiceAbstraction {
pinLockType: PinLockType,
): Promise<PasswordProtectedKeyEnvelope | null>;
/**
* Gets the user's legacy PIN-protected UserKey
* @deprecated Use {@link getPinProtectedUserKeyEnvelope} instead. Only for migration support.
* @param userId The user's id
* @throws If the user id is not provided
*/
abstract getLegacyPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<EncString | null>;
/**
* Sets the PIN state for the user
* @deprecated - This is not a public API. DO NOT USE IT

View File

@@ -13,7 +13,6 @@ import {
PIN_PROTECTED_USER_KEY_ENVELOPE_PERSISTENT,
PIN_PROTECTED_USER_KEY_ENVELOPE_EPHEMERAL,
USER_KEY_ENCRYPTED_PIN,
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
} from "./pin.state";
export class PinStateService implements PinStateServiceAbstraction {
@@ -36,9 +35,7 @@ export class PinStateService implements PinStateServiceAbstraction {
assertNonNullish(userId, "userId");
const isPersistentPinSet =
(await this.getPinProtectedUserKeyEnvelope(userId, "PERSISTENT")) != null ||
// Deprecated
(await this.getLegacyPinKeyEncryptedUserKeyPersistent(userId)) != null;
(await this.getPinProtectedUserKeyEnvelope(userId, "PERSISTENT")) != null;
const isPinSet =
(await firstValueFrom(this.stateProvider.getUserState$(USER_KEY_ENCRYPTED_PIN, userId))) !=
null;
@@ -71,16 +68,6 @@ export class PinStateService implements PinStateServiceAbstraction {
}
}
async getLegacyPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<EncString | null> {
assertNonNullish(userId, "userId");
return await firstValueFrom(
this.stateProvider
.getUserState$(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, userId)
.pipe(map((value) => (value ? new EncString(value) : null))),
);
}
async setPinState(
userId: UserId,
pinProtectedUserKeyEnvelope: PasswordProtectedKeyEnvelope,
@@ -116,9 +103,6 @@ export class PinStateService implements PinStateServiceAbstraction {
await this.stateProvider.setUserState(USER_KEY_ENCRYPTED_PIN, null, userId);
await this.stateProvider.setUserState(PIN_PROTECTED_USER_KEY_ENVELOPE_EPHEMERAL, null, userId);
await this.stateProvider.setUserState(PIN_PROTECTED_USER_KEY_ENVELOPE_PERSISTENT, null, userId);
// Note: This can be deleted after sufficiently many PINs are migrated and the state is removed.
await this.stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, null, userId);
}
async clearEphemeralPinState(userId: UserId): Promise<void> {

View File

@@ -13,7 +13,6 @@ import {
USER_KEY_ENCRYPTED_PIN,
PIN_PROTECTED_USER_KEY_ENVELOPE_EPHEMERAL,
PIN_PROTECTED_USER_KEY_ENVELOPE_PERSISTENT,
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
} from "./pin.state";
describe("PinStateService", () => {
@@ -121,21 +120,6 @@ describe("PinStateService", () => {
expect(result).toBe("PERSISTENT");
});
it("should return 'PERSISTENT' if a legacy pin key encrypted user key (persistent) is found", async () => {
// Arrange
await stateProvider.setUserState(
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
mockUserKeyEncryptedPin,
mockUserId,
);
// Act
const result = await sut.getPinLockType(mockUserId);
// Assert
expect(result).toBe("PERSISTENT");
});
it("should return 'EPHEMERAL' if only user key encrypted pin is found", async () => {
// Arrange
await stateProvider.setUserState(USER_KEY_ENCRYPTED_PIN, mockUserKeyEncryptedPin, mockUserId);
@@ -164,7 +148,6 @@ describe("PinStateService", () => {
null,
mockUserId,
);
await stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, null, mockUserId);
await stateProvider.setUserState(USER_KEY_ENCRYPTED_PIN, null, mockUserId);
// Act
@@ -290,45 +273,6 @@ describe("PinStateService", () => {
});
});
describe("getLegacyPinKeyEncryptedUserKeyPersistent()", () => {
beforeEach(() => {
jest.clearAllMocks();
});
test.each([null, undefined])("throws if userId is %p", async (userId) => {
// Act & Assert
await expect(() =>
sut.getLegacyPinKeyEncryptedUserKeyPersistent(userId as any),
).rejects.toThrow("userId is null or undefined.");
});
it("should return EncString when legacy key is set", async () => {
// Arrange
await stateProvider.setUserState(
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
mockUserKeyEncryptedPin,
mockUserId,
);
// Act
const result = await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId);
// Assert
expect(result?.encryptedString).toEqual(mockUserKeyEncryptedPin);
});
test.each([null, undefined])("should return null when legacy key is %p", async (value) => {
// Arrange
await stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, value, mockUserId);
// Act
const result = await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId);
// Assert
expect(result).toBeNull();
});
});
describe("setPinState()", () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -464,22 +408,6 @@ describe("PinStateService", () => {
expect(result).toBeNull();
});
it("clears legacy PIN key encrypted user key persistent", async () => {
// Arrange
await stateProvider.setUserState(
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
mockUserKeyEncryptedPin,
mockUserId,
);
// Act
await sut.clearPinState(mockUserId);
// Assert
const result = await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId);
expect(result).toBeNull();
});
it("clears all PIN state when all types are set", async () => {
// Arrange - set up all possible PIN state
await sut.setPinState(
@@ -494,17 +422,11 @@ describe("PinStateService", () => {
mockUserKeyEncryptedPin,
"EPHEMERAL",
);
await stateProvider.setUserState(
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
mockUserKeyEncryptedPin,
mockUserId,
);
// Verify all state is set before clearing
expect(await firstValueFrom(sut.userKeyEncryptedPin$(mockUserId))).not.toBeNull();
expect(await sut.getPinProtectedUserKeyEnvelope(mockUserId, "EPHEMERAL")).not.toBeNull();
expect(await sut.getPinProtectedUserKeyEnvelope(mockUserId, "PERSISTENT")).not.toBeNull();
expect(await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId)).not.toBeNull();
// Act
await sut.clearPinState(mockUserId);
@@ -513,7 +435,6 @@ describe("PinStateService", () => {
expect(await firstValueFrom(sut.userKeyEncryptedPin$(mockUserId))).toBeNull();
expect(await sut.getPinProtectedUserKeyEnvelope(mockUserId, "EPHEMERAL")).toBeNull();
expect(await sut.getPinProtectedUserKeyEnvelope(mockUserId, "PERSISTENT")).toBeNull();
expect(await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId)).toBeNull();
});
it("results in PIN lock type DISABLED after clearing", async () => {
@@ -545,7 +466,6 @@ describe("PinStateService", () => {
expect(await firstValueFrom(sut.userKeyEncryptedPin$(mockUserId))).toBeNull();
expect(await sut.getPinProtectedUserKeyEnvelope(mockUserId, "EPHEMERAL")).toBeNull();
expect(await sut.getPinProtectedUserKeyEnvelope(mockUserId, "PERSISTENT")).toBeNull();
expect(await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId)).toBeNull();
expect(await sut.getPinLockType(mockUserId)).toBe("DISABLED");
});
});
@@ -623,32 +543,6 @@ describe("PinStateService", () => {
expect(ephemeralResult).toBeNull();
});
it("does not clear legacy PIN key encrypted user key persistent", async () => {
// Arrange - set up ephemeral state and legacy state
await sut.setPinState(
mockUserId,
mockEphemeralEnvelope,
mockUserKeyEncryptedPin,
"EPHEMERAL",
);
await stateProvider.setUserState(
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
mockUserKeyEncryptedPin,
mockUserId,
);
// Act
await sut.clearEphemeralPinState(mockUserId);
// Assert - legacy PIN should still be present
const legacyResult = await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId);
expect(legacyResult?.encryptedString).toEqual(mockUserKeyEncryptedPin);
// Assert - ephemeral envelope should be cleared
const ephemeralResult = await sut.getPinProtectedUserKeyEnvelope(mockUserId, "EPHEMERAL");
expect(ephemeralResult).toBeNull();
});
it("changes PIN lock type from EPHEMERAL to DISABLED when no other PIN state exists", async () => {
// Arrange - set up only ephemeral PIN state
await sut.setPinState(

View File

@@ -1,8 +1,5 @@
// eslint-disable-next-line no-restricted-imports
import { KdfConfig } from "@bitwarden/key-management";
import { UserId } from "../../types/guid";
import { PinKey, UserKey } from "../../types/key";
import { UserKey } from "../../types/key";
import { PinLockType } from "./pin-lock-type";
@@ -69,10 +66,4 @@ export abstract class PinServiceAbstraction {
* @deprecated This is not deprecated, but only meant to be called by KeyService. DO NOT USE IT.
*/
abstract userUnlocked(userId: UserId): Promise<void>;
/**
* Makes a PinKey from the provided PIN.
* @deprecated - Note: This is currently re-used by vault exports, which is still permitted but should be refactored out to use a different construct.
*/
abstract makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey>;
}

View File

@@ -1,18 +1,15 @@
import { firstValueFrom, map } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
import { KeyService } from "@bitwarden/key-management";
import { AccountService } from "../../auth/abstractions/account.service";
import { assertNonNullish } from "../../auth/utils";
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
import { EncString } from "../../key-management/crypto/models/enc-string";
import { LogService } from "../../platform/abstractions/log.service";
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { UserId } from "../../types/guid";
import { PinKey, UserKey } from "../../types/key";
import { KeyGenerationService } from "../crypto";
import { UserKey } from "../../types/key";
import { firstValueFromOrThrow } from "../utils";
import { PinLockType } from "./pin-lock-type";
@@ -21,10 +18,7 @@ import { PinServiceAbstraction } from "./pin.service.abstraction";
export class PinService implements PinServiceAbstraction {
constructor(
private accountService: AccountService,
private encryptService: EncryptService,
private kdfConfigService: KdfConfigService,
private keyGenerationService: KeyGenerationService,
private logService: LogService,
private keyService: KeyService,
private sdkService: SdkService,
@@ -56,19 +50,6 @@ export class PinService implements PinServiceAbstraction {
// On first unlock, set the ephemeral pin envelope, if it is not set yet
const pin = await this.getPin(userId);
await this.setPin(pin, "EPHEMERAL", userId);
} else if ((await this.pinStateService.getPinLockType(userId)) === "PERSISTENT") {
// Encrypted migration for persistent pin unlock to pin envelopes.
// This will be removed at the earliest in 2026.1.0
//
// ----- ENCRYPTION MIGRATION -----
// Pin-key encrypted user-keys are eagerly migrated to the new pin-protected user key envelope format.
if ((await this.pinStateService.getLegacyPinKeyEncryptedUserKeyPersistent(userId)) != null) {
this.logService.info(
"[Pin Service] Migrating legacy PIN key to PinProtectedUserKeyEnvelope",
);
const pin = await this.getPin(userId);
await this.setPin(pin, "PERSISTENT", userId);
}
}
}
@@ -144,86 +125,30 @@ export class PinService implements PinServiceAbstraction {
assertNonNullish(pin, "pin");
assertNonNullish(userId, "userId");
const hasPinProtectedKeyEnvelopeSet =
(await this.pinStateService.getPinProtectedUserKeyEnvelope(userId, "EPHEMERAL")) != null ||
(await this.pinStateService.getPinProtectedUserKeyEnvelope(userId, "PERSISTENT")) != null;
this.logService.info("[Pin Service] Pin-unlock via PinProtectedUserKeyEnvelope");
if (hasPinProtectedKeyEnvelopeSet) {
this.logService.info("[Pin Service] Pin-unlock via PinProtectedUserKeyEnvelope");
const pinLockType = await this.pinStateService.getPinLockType(userId);
const envelope = await this.pinStateService.getPinProtectedUserKeyEnvelope(userId, pinLockType);
const pinLockType = await this.pinStateService.getPinLockType(userId);
const envelope = await this.pinStateService.getPinProtectedUserKeyEnvelope(
userId,
pinLockType,
try {
// Use the sdk to create an enrollment, not yet persisting it to state
const startTime = performance.now();
const userKeyBytes = await firstValueFrom(
this.sdkService.client$.pipe(
map((sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
return sdk.crypto().unseal_password_protected_key_envelope(pin, envelope!);
}),
),
);
this.logService.measure(startTime, "Crypto", "PinService", "UnsealPinEnvelope");
try {
// Use the sdk to create an enrollment, not yet persisting it to state
const startTime = performance.now();
const userKeyBytes = await firstValueFrom(
this.sdkService.client$.pipe(
map((sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
return sdk.crypto().unseal_password_protected_key_envelope(pin, envelope!);
}),
),
);
this.logService.measure(startTime, "Crypto", "PinService", "UnsealPinEnvelope");
return new SymmetricCryptoKey(userKeyBytes) as UserKey;
} catch (error) {
this.logService.error(`Failed to unseal pin: ${error}`);
return null;
}
} else {
this.logService.info("[Pin Service] Pin-unlock via legacy PinKeyEncryptedUserKey");
// This branch is deprecated and will be removed in the future, but is kept for migration.
try {
const pinKeyEncryptedUserKey =
await this.pinStateService.getLegacyPinKeyEncryptedUserKeyPersistent(userId);
const email = await firstValueFrom(
this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)),
);
const kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
return await this.decryptUserKey(pin, email, kdfConfig, pinKeyEncryptedUserKey!);
} catch (error) {
this.logService.error(`Error decrypting user key with pin: ${error}`);
return null;
}
return new SymmetricCryptoKey(userKeyBytes) as UserKey;
} catch (error) {
this.logService.error(`Failed to unseal pin: ${error}`);
return null;
}
}
/// Anything below here is deprecated and will be removed subsequently
async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey> {
const startTime = performance.now();
const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig);
this.logService.measure(startTime, "Crypto", "PinService", "makePinKey");
return (await this.keyGenerationService.stretchKey(pinKey)) as PinKey;
}
/**
* Decrypts the UserKey with the provided PIN.
* @deprecated
* @throws If the PIN does not match the PIN that was used to encrypt the user key
* @throws If the salt, or KDF don't match the salt / KDF used to encrypt the user key
*/
private async decryptUserKey(
pin: string,
salt: string,
kdfConfig: KdfConfig,
pinKeyEncryptedUserKey: EncString,
): Promise<UserKey> {
assertNonNullish(pin, "pin");
assertNonNullish(salt, "salt");
assertNonNullish(kdfConfig, "kdfConfig");
assertNonNullish(pinKeyEncryptedUserKey, "pinKeyEncryptedUserKey");
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
const userKey = await this.encryptService.unwrapSymmetricKey(pinKeyEncryptedUserKey, pinKey);
return userKey as UserKey;
}
}

View File

@@ -2,17 +2,15 @@ import { mock } from "jest-mock-extended";
import { BehaviorSubject, filter } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
import { KeyService } from "@bitwarden/key-management";
import { PasswordProtectedKeyEnvelope } from "@bitwarden/sdk-internal";
import { MockSdkService } from "../..//platform/spec/mock-sdk.service";
import { FakeAccountService, mockAccountServiceWith, mockEnc } from "../../../spec";
import { LogService } from "../../platform/abstractions/log.service";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { UserId } from "../../types/guid";
import { PinKey, UserKey } from "../../types/key";
import { KeyGenerationService } from "../crypto";
import { UserKey } from "../../types/key";
import { EncryptService } from "../crypto/abstractions/encrypt.service";
import { EncryptedString, EncString } from "../crypto/models/enc-string";
@@ -22,16 +20,10 @@ import { PinService } from "./pin.service.implementation";
describe("PinService", () => {
let sut: PinService;
let accountService: FakeAccountService;
const encryptService = mock<EncryptService>();
const kdfConfigService = mock<KdfConfigService>();
const keyGenerationService = mock<KeyGenerationService>();
const logService = mock<LogService>();
const mockUserId = Utils.newGuid() as UserId;
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
const mockPinKey = new SymmetricCryptoKey(randomBytes(32)) as PinKey;
const mockUserEmail = "user@example.com";
const mockPin = "1234";
const mockUserKeyEncryptedPin = new EncString("userKeyEncryptedPin");
const mockEphemeralEnvelope = "mock-ephemeral-envelope" as PasswordProtectedKeyEnvelope;
@@ -42,7 +34,6 @@ describe("PinService", () => {
const behaviorSubject = new BehaviorSubject<{ userId: UserId; userKey: UserKey }>(null);
beforeEach(() => {
accountService = mockAccountServiceWith(mockUserId, { email: mockUserEmail });
(keyService as any)["unlockedUserKeys$"] = behaviorSubject
.asObservable()
.pipe(filter((x) => x != null));
@@ -50,16 +41,7 @@ describe("PinService", () => {
.mockDeep()
.unseal_password_protected_key_envelope.mockReturnValue(new Uint8Array(64));
sut = new PinService(
accountService,
encryptService,
kdfConfigService,
keyGenerationService,
logService,
keyService,
sdkService,
pinStateService,
);
sut = new PinService(encryptService, logService, keyService, sdkService, pinStateService);
});
it("should instantiate the PinService", () => {
@@ -89,26 +71,6 @@ describe("PinService", () => {
);
});
it("should migrate legacy persistent PIN if needed", async () => {
// Arrange
pinStateService.getPinLockType.mockResolvedValue("PERSISTENT");
pinStateService.getLegacyPinKeyEncryptedUserKeyPersistent.mockResolvedValue(
mockEnc("legacy-key"),
);
const getPinSpy = jest.spyOn(sut, "getPin").mockResolvedValue(mockPin);
const setPinSpy = jest.spyOn(sut, "setPin").mockResolvedValue();
// Act
await sut.userUnlocked(mockUserId);
// Assert
expect(getPinSpy).toHaveBeenCalledWith(mockUserId);
expect(setPinSpy).toHaveBeenCalledWith(mockPin, "PERSISTENT", mockUserId);
expect(logService.info).toHaveBeenCalledWith(
"[Pin Service] Migrating legacy PIN key to PinProtectedUserKeyEnvelope",
);
});
it("should do nothing if no migration or setup is needed", async () => {
// Arrange
pinStateService.getPinLockType.mockResolvedValue("DISABLED");
@@ -124,28 +86,6 @@ describe("PinService", () => {
});
});
describe("makePinKey()", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("should make a PinKey", async () => {
// Arrange
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(mockPinKey);
// Act
await sut.makePinKey(mockPin, mockUserEmail, DEFAULT_KDF_CONFIG);
// Assert
expect(keyGenerationService.deriveKeyFromPassword).toHaveBeenCalledWith(
mockPin,
mockUserEmail,
DEFAULT_KDF_CONFIG,
);
expect(keyGenerationService.stretchKey).toHaveBeenCalledWith(mockPinKey);
});
});
describe("getPin()", () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -383,7 +323,6 @@ describe("PinService", () => {
jest.clearAllMocks();
pinStateService.userKeyEncryptedPin$.mockReset();
pinStateService.getPinProtectedUserKeyEnvelope.mockReset();
pinStateService.getLegacyPinKeyEncryptedUserKeyPersistent.mockReset();
});
it("should throw an error if userId is null", async () => {
@@ -423,32 +362,5 @@ describe("PinService", () => {
// Assert
expect(result).toEqual(mockUserKey);
});
it("should return userkey with legacy pin PERSISTENT", async () => {
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(mockPinKey);
keyGenerationService.stretchKey.mockResolvedValue(mockPinKey);
kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG);
encryptService.unwrapSymmetricKey.mockResolvedValue(mockUserKey);
// Arrange
const mockPin = "1234";
pinStateService.userKeyEncryptedPin$.mockReturnValueOnce(
new BehaviorSubject(mockUserKeyEncryptedPin),
);
pinStateService.getLegacyPinKeyEncryptedUserKeyPersistent.mockResolvedValueOnce(
mockUserKeyEncryptedPin,
);
// Act
const result = await sut.decryptUserKeyWithPin(mockPin, mockUserId);
// Assert
expect(result).toEqual(mockUserKey);
});
});
});
// Test helpers
function randomBytes(length: number): Uint8Array {
return new Uint8Array(Array.from({ length }, (_, k) => k % 255));
}

View File

@@ -3,22 +3,6 @@ import { PasswordProtectedKeyEnvelope } from "@bitwarden/sdk-internal";
import { EncryptedString } from "../crypto/models/enc-string";
/**
* The persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
*
* @deprecated
* @remarks Persists through a client reset. Used when `requireMasterPasswordOnClientRestart` is disabled.
* @see SetPinComponent.setPinForm.requireMasterPasswordOnClientRestart
*/
export const PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT = new UserKeyDefinition<EncryptedString>(
PIN_DISK,
"pinKeyEncryptedUserKeyPersistent",
{
deserializer: (jsonValue) => jsonValue,
clearOn: ["logout"],
},
);
/**
* The persistent (stored on disk) version of the UserKey, stored in a `PasswordProtectedKeyEnvelope`.
*

View File

@@ -56,7 +56,7 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract
return;
}
// If there is an active user, check if they have a pinKeyEncryptedUserKeyEphemeral. If so, prevent process reload upon lock.
// If there is an active user, check if they have an ephemeral PIN. If so, prevent process reload upon lock.
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (userId != null) {
if ((await this.pinService.getPinLockType(userId)) === "EPHEMERAL") {

View File

@@ -7,7 +7,7 @@ import { BehaviorSubject, from, of } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { LockService, LogoutService } from "@bitwarden/auth/common";
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
import { FakeAccountService, mockAccountServiceWith, mockAccountInfoWith } from "../../../../spec";
import { AccountInfo } from "../../../auth/abstractions/account.service";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
@@ -109,19 +109,19 @@ describe("VaultTimeoutService", () => {
if (globalSetups?.userId) {
accountService.activeAccountSubject.next({
id: globalSetups.userId as UserId,
email: null,
emailVerified: false,
name: null,
...mockAccountInfoWith({
email: null,
name: null,
}),
});
}
accountService.accounts$ = of(
Object.entries(accounts).reduce(
(agg, [id]) => {
agg[id] = {
agg[id] = mockAccountInfoWith({
email: "",
emailVerified: true,
name: "",
};
});
return agg;
},
{} as Record<string, AccountInfo>,

View File

@@ -5,8 +5,10 @@ export interface IpcMessage {
message: SerializedOutgoingMessage;
}
export interface SerializedOutgoingMessage
extends Omit<OutgoingMessage, typeof Symbol.dispose | "free" | "payload"> {
export interface SerializedOutgoingMessage extends Omit<
OutgoingMessage,
typeof Symbol.dispose | "free" | "payload"
> {
payload: number[];
}

View File

@@ -7,6 +7,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { mockAccountInfoWith } from "../../../../spec";
import { AccountService } from "../../../auth/abstractions/account.service";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
@@ -163,9 +164,10 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
} else {
activeUserAccount$.next({
id: userId,
email: "email",
name: "Test Name",
emailVerified: true,
...mockAccountInfoWith({
email: "email",
name: "Test Name",
}),
});
}
}
@@ -174,7 +176,10 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
const currentAccounts = (userAccounts$.getValue() as Record<string, any>) ?? {};
userAccounts$.next({
...currentAccounts,
[userId]: { email: "email", name: "Test Name", emailVerified: true },
[userId]: mockAccountInfoWith({
email: "email",
name: "Test Name",
}),
} as any);
}

View File

@@ -8,7 +8,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
import { awaitAsync } from "../../../../spec";
import { awaitAsync, mockAccountInfoWith } from "../../../../spec";
import { Matrix } from "../../../../spec/matrix";
import { AccountService } from "../../../auth/abstractions/account.service";
import { AuthService } from "../../../auth/abstractions/auth.service";
@@ -139,11 +139,18 @@ describe("NotificationsService", () => {
activeAccount.next(null);
accounts.next({} as any);
} else {
activeAccount.next({ id: userId, email: "email", name: "Test Name", emailVerified: true });
const accountInfo = mockAccountInfoWith({
email: "email",
name: "Test Name",
});
activeAccount.next({
id: userId,
...accountInfo,
});
const current = (accounts.getValue() as Record<string, any>) ?? {};
accounts.next({
...current,
[userId]: { email: "email", name: "Test Name", emailVerified: true },
[userId]: accountInfo,
} as any);
}
}
@@ -349,7 +356,13 @@ describe("NotificationsService", () => {
describe("processNotification", () => {
beforeEach(async () => {
appIdService.getAppId.mockResolvedValue("test-app-id");
activeAccount.next({ id: mockUser1, email: "email", name: "Test Name", emailVerified: true });
activeAccount.next({
id: mockUser1,
...mockAccountInfoWith({
email: "email",
name: "Test Name",
}),
});
});
describe("NotificationType.LogOut", () => {

View File

@@ -1,6 +1,6 @@
import { firstValueFrom } from "rxjs";
import { FakeStateProvider, awaitAsync } from "../../../spec";
import { FakeStateProvider, awaitAsync, mockAccountInfoWith } from "../../../spec";
import { FakeAccountService } from "../../../spec/fake-account-service";
import { UserId } from "../../types/guid";
import { CloudRegion, Region } from "../abstractions/environment.service";
@@ -28,16 +28,14 @@ describe("EnvironmentService", () => {
beforeEach(async () => {
accountService = new FakeAccountService({
[testUser]: {
[testUser]: mockAccountInfoWith({
name: "name",
email: "email",
emailVerified: false,
},
[alternateTestUser]: {
}),
[alternateTestUser]: mockAccountInfoWith({
name: "name",
email: "email",
emailVerified: false,
},
}),
});
stateProvider = new FakeStateProvider(accountService);
@@ -47,9 +45,10 @@ describe("EnvironmentService", () => {
const switchUser = async (userId: UserId) => {
accountService.activeAccountSubject.next({
id: userId,
email: "test@example.com",
name: `Test Name ${userId}`,
emailVerified: false,
...mockAccountInfoWith({
email: "test@example.com",
name: `Test Name ${userId}`,
}),
});
await awaitAsync();
};

View File

@@ -3,7 +3,7 @@ import { TextEncoder } from "util";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { mockAccountServiceWith } from "../../../../spec";
import { mockAccountServiceWith, mockAccountInfoWith } from "../../../../spec";
import { Account } from "../../../auth/abstractions/account.service";
import { CipherId, UserId } from "../../../types/guid";
import { CipherService, EncryptionContext } from "../../../vault/abstractions/cipher.service";
@@ -40,9 +40,10 @@ describe("FidoAuthenticatorService", () => {
const userId = "testId" as UserId;
const activeAccountSubject = new BehaviorSubject<Account | null>({
id: userId,
email: "test@example.com",
emailVerified: true,
name: "Test User",
...mockAccountInfoWith({
email: "test@example.com",
name: "Test User",
}),
});
let cipherService!: MockProxy<CipherService>;

View File

@@ -46,9 +46,9 @@ const KeyUsages: KeyUsage[] = ["sign"];
*
* It is highly recommended that the W3C specification is used a reference when reading this code.
*/
export class Fido2AuthenticatorService<ParentWindowReference>
implements Fido2AuthenticatorServiceAbstraction<ParentWindowReference>
{
export class Fido2AuthenticatorService<
ParentWindowReference,
> implements Fido2AuthenticatorServiceAbstraction<ParentWindowReference> {
constructor(
private cipherService: CipherService,
private userInterface: Fido2UserInterfaceService<ParentWindowReference>,

View File

@@ -47,9 +47,9 @@ import { guidToRawFormat } from "./guid-utils";
*
* It is highly recommended that the W3C specification is used a reference when reading this code.
*/
export class Fido2ClientService<ParentWindowReference>
implements Fido2ClientServiceAbstraction<ParentWindowReference>
{
export class Fido2ClientService<
ParentWindowReference,
> implements Fido2ClientServiceAbstraction<ParentWindowReference> {
private timeoutAbortController: AbortController;
private readonly TIMEOUTS = {
NO_VERIFICATION: {

View File

@@ -12,9 +12,9 @@ import {
FakeAccountService,
FakeStateProvider,
mockAccountServiceWith,
mockAccountInfoWith,
} from "../../../../spec";
import { ApiService } from "../../../abstractions/api.service";
import { AccountInfo } from "../../../auth/abstractions/account.service";
import { EncryptedString } from "../../../key-management/crypto/models/enc-string";
import { UserId } from "../../../types/guid";
import { UserKey } from "../../../types/key";
@@ -92,7 +92,10 @@ describe("DefaultSdkService", () => {
.calledWith(userId)
.mockReturnValue(new BehaviorSubject(mock<Environment>()));
accountService.accounts$ = of({
[userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo,
[userId]: mockAccountInfoWith({
email: "email",
name: "name",
}),
});
kdfConfigService.getKdfConfig$
.calledWith(userId)

View File

@@ -8,9 +8,9 @@ import {
FakeAccountService,
FakeStateProvider,
mockAccountServiceWith,
mockAccountInfoWith,
} from "../../../../spec";
import { ApiService } from "../../../abstractions/api.service";
import { AccountInfo } from "../../../auth/abstractions/account.service";
import { UserId } from "../../../types/guid";
import { ConfigService } from "../../abstractions/config/config.service";
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
@@ -76,7 +76,10 @@ describe("DefaultRegisterSdkService", () => {
.calledWith(userId)
.mockReturnValue(new BehaviorSubject(mock<Environment>()));
accountService.accounts$ = of({
[userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo,
[userId]: mockAccountInfoWith({
email: "email",
name: "name",
}),
});
});
@@ -125,7 +128,10 @@ describe("DefaultRegisterSdkService", () => {
it("destroys the internal SDK client when the account is removed (logout)", async () => {
const accounts$ = new BehaviorSubject({
[userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo,
[userId]: mockAccountInfoWith({
email: "email",
name: "name",
}),
});
accountService.accounts$ = accounts$;

View File

@@ -272,6 +272,7 @@ export class DefaultSyncService extends CoreSyncService {
await this.tokenService.setSecurityStamp(response.securityStamp, response.id);
await this.accountService.setAccountEmailVerified(response.id, response.emailVerified);
await this.accountService.setAccountVerifyNewDeviceLogin(response.id, response.verifyDevices);
await this.accountService.setAccountCreationDate(response.id, response.creationDate);
await this.billingAccountProfileStateService.setHasPremium(
response.premiumPersonally,

View File

@@ -6,6 +6,7 @@ import { ObservedValueOf, of } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { UserId } from "@bitwarden/user-core";
import { mockAccountInfoWith } from "../../spec";
import { AccountService } from "../auth/abstractions/account.service";
import { TokenService } from "../auth/abstractions/token.service";
import { DeviceType } from "../enums";
@@ -55,9 +56,10 @@ describe("ApiService", () => {
accountService.activeAccount$ = of({
id: testActiveUser,
email: "user1@example.com",
emailVerified: true,
name: "Test Name",
...mockAccountInfoWith({
email: "user1@example.com",
name: "Test Name",
}),
} satisfies ObservedValueOf<AccountService["activeAccount$"]>);
httpOperations = mock();

View File

@@ -63,6 +63,7 @@ import { UpdateProfileRequest } from "../auth/models/request/update-profile.requ
import { ApiKeyResponse } from "../auth/models/response/api-key.response";
import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response";
import { IdentitySsoRequiredResponse } from "../auth/models/response/identity-sso-required.response";
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
@@ -165,7 +166,10 @@ export class ApiService implements ApiServiceAbstraction {
| SsoTokenRequest
| WebAuthnLoginTokenRequest,
): Promise<
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
| IdentityTokenResponse
| IdentityTwoFactorResponse
| IdentityDeviceVerificationResponse
| IdentitySsoRequiredResponse
> {
const headers = new Headers({
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
@@ -212,6 +216,8 @@ export class ApiService implements ApiServiceAbstraction {
responseJson?.ErrorModel?.Message === ApiService.NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE
) {
return new IdentityDeviceVerificationResponse(responseJson);
} else if (response.status === 400 && responseJson?.SsoOrganizationIdentifier) {
return new IdentitySsoRequiredResponse(responseJson);
}
}

View File

@@ -1,7 +1,12 @@
import { mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { FakeAccountService, FakeStateProvider, awaitAsync } from "../../../spec";
import {
FakeAccountService,
FakeStateProvider,
awaitAsync,
mockAccountInfoWith,
} from "../../../spec";
import { Account } from "../../auth/abstractions/account.service";
import { EXTENSION_DISK, UserKeyDefinition } from "../../platform/state";
import { UserId } from "../../types/guid";
@@ -21,9 +26,10 @@ import { SimpleLogin } from "./vendor/simplelogin";
const SomeUser = "some user" as UserId;
const SomeAccount = {
id: SomeUser,
email: "someone@example.com",
emailVerified: true,
name: "Someone",
...mockAccountInfoWith({
email: "someone@example.com",
name: "Someone",
}),
};
const SomeAccount$ = new BehaviorSubject<Account>(SomeAccount);

View File

@@ -11,6 +11,7 @@ import {
FakeStateProvider,
awaitAsync,
mockAccountServiceWith,
mockAccountInfoWith,
} from "../../../../spec";
import { KeyGenerationService } from "../../../key-management/crypto";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
@@ -71,9 +72,10 @@ describe("SendService", () => {
accountService.activeAccountSubject.next({
id: mockUserId,
email: "email",
emailVerified: false,
name: "name",
...mockAccountInfoWith({
email: "email",
name: "name",
}),
});
// Initial encrypted state

View File

@@ -14,9 +14,11 @@ import { Classifier } from "./classifier";
* Data that cannot be serialized by JSON.stringify() should
* be excluded.
*/
export class SecretClassifier<Plaintext extends object, Disclosed, Secret>
implements Classifier<Plaintext, Disclosed, Secret>
{
export class SecretClassifier<Plaintext extends object, Disclosed, Secret> implements Classifier<
Plaintext,
Disclosed,
Secret
> {
private constructor(
disclosed: readonly (keyof Jsonify<Disclosed> & keyof Jsonify<Plaintext>)[],
excluded: readonly (keyof Plaintext)[],

View File

@@ -25,9 +25,13 @@ const ONE_MINUTE = 1000 * 60;
*
* DO NOT USE THIS for synchronized data.
*/
export class SecretState<Outer, Id, Plaintext extends object, Disclosed, Secret>
implements SingleUserState<Outer>
{
export class SecretState<
Outer,
Id,
Plaintext extends object,
Disclosed,
Secret,
> implements SingleUserState<Outer> {
// The constructor is private to avoid creating a circular dependency when
// wiring the derived and secret states together.
private constructor(

View File

@@ -6,6 +6,7 @@ import {
awaitAsync,
FakeAccountService,
FakeStateProvider,
mockAccountInfoWith,
ObservableTracker,
} from "../../../spec";
import { Account } from "../../auth/abstractions/account.service";
@@ -23,17 +24,19 @@ import { UserStateSubject } from "./user-state-subject";
const SomeUser = "some user" as UserId;
const SomeAccount = {
id: SomeUser,
email: "someone@example.com",
emailVerified: true,
name: "Someone",
...mockAccountInfoWith({
email: "someone@example.com",
name: "Someone",
}),
};
const SomeAccount$ = new BehaviorSubject<Account>(SomeAccount);
const SomeOtherAccount = {
id: "some other user" as UserId,
email: "someone@example.com",
emailVerified: true,
name: "Someone",
...mockAccountInfoWith({
email: "someone@example.com",
name: "Someone",
}),
};
type TestType = { foo: string };

View File

@@ -79,11 +79,11 @@ const DEFAULT_FRAME_SIZE = 32;
* @template Dependencies use-specific dependencies provided by the user.
*/
export class UserStateSubject<
State extends object,
Secret = State,
Disclosed = Record<string, never>,
Dependencies = null,
>
State extends object,
Secret = State,
Disclosed = Record<string, never>,
Dependencies = null,
>
extends Observable<State>
implements SubjectLike<State>
{

View File

@@ -9,8 +9,6 @@ export type PrfKey = Opaque<SymmetricCryptoKey, "PrfKey">;
export type UserKey = Opaque<SymmetricCryptoKey, "UserKey">;
/** @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead. */
export type MasterKey = Opaque<SymmetricCryptoKey, "MasterKey">;
/** @deprecated */
export type PinKey = Opaque<SymmetricCryptoKey, "PinKey">;
export type OrgKey = Opaque<SymmetricCryptoKey, "OrgKey">;
export type ProviderKey = Opaque<SymmetricCryptoKey, "ProviderKey">;
export type CipherKey = Opaque<SymmetricCryptoKey, "CipherKey">;

View File

@@ -13,18 +13,19 @@ describe("buildCipherIcon", () => {
},
} as any as CipherView;
it.each([true, false])("handles android app URIs for showFavicon setting %s", (showFavicon) => {
setUri("androidapp://test.example");
// @TODO Uncomment once we have Android and iOS icons https://bitwarden.atlassian.net/browse/PM-29028
// it.each([true, false])("handles android app URIs for showFavicon setting %s", (showFavicon) => {
// setUri("androidapp://test.example");
const iconDetails = buildCipherIcon(iconServerUrl, cipher, showFavicon);
// const iconDetails = buildCipherIcon(iconServerUrl, cipher, showFavicon);
expect(iconDetails).toEqual({
icon: "bwi-android",
image: null,
fallbackImage: "",
imageEnabled: showFavicon,
});
});
// expect(iconDetails).toEqual({
// icon: "bwi-android",
// image: null,
// fallbackImage: "",
// imageEnabled: showFavicon,
// });
// });
it("does not mark as an android app if the protocol is not androidapp", () => {
// This weird URI points to test.androidapp with a default port and path of /.example
@@ -40,18 +41,18 @@ describe("buildCipherIcon", () => {
});
});
it.each([true, false])("handles ios app URIs for showFavicon setting %s", (showFavicon) => {
setUri("iosapp://test.example");
// @TODO Uncomment once we have Android and iOS icons https://bitwarden.atlassian.net/browse/PM-29028
// it.each([true, false])("handles ios app URIs for showFavicon setting %s", (showFavicon) => {
// setUri("iosapp://test.example");
const iconDetails = buildCipherIcon(iconServerUrl, cipher, showFavicon);
expect(iconDetails).toEqual({
icon: "bwi-apple",
image: null,
fallbackImage: "",
imageEnabled: showFavicon,
});
});
// const iconDetails = buildCipherIcon(iconServerUrl, cipher, showFavicon);
// expect(iconDetails).toEqual({
// icon: "bwi-apple",
// image: null,
// fallbackImage: "",
// imageEnabled: showFavicon,
// });
// });
it("does not mark as an ios app if the protocol is not iosapp", () => {
// This weird URI points to test.iosapp with a default port and path of /.example

View File

@@ -49,10 +49,12 @@ export function buildCipherIcon(
let isWebsite = false;
if (hostnameUri.indexOf("androidapp://") === 0) {
icon = "bwi-android";
// @TODO Re-add once we have Android icon https://bitwarden.atlassian.net/browse/PM-29028
// icon = "bwi-android";
image = null;
} else if (hostnameUri.indexOf("iosapp://") === 0) {
icon = "bwi-apple";
// @TODO Re-add once we have iOS icon https://bitwarden.atlassian.net/browse/PM-29028
// icon = "bwi-apple";
image = null;
} else if (
showFavicon &&

View File

@@ -155,7 +155,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
if (this.login != null) {
model.login = await this.login.decrypt(
bypassValidation,
userKeyOrOrgKey,
cipherDecryptionKey,
`Cipher Id: ${this.id}`,
);
}
@@ -167,17 +167,20 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
break;
case CipherType.Card:
if (this.card != null) {
model.card = await this.card.decrypt(userKeyOrOrgKey, `Cipher Id: ${this.id}`);
model.card = await this.card.decrypt(cipherDecryptionKey, `Cipher Id: ${this.id}`);
}
break;
case CipherType.Identity:
if (this.identity != null) {
model.identity = await this.identity.decrypt(userKeyOrOrgKey, `Cipher Id: ${this.id}`);
model.identity = await this.identity.decrypt(
cipherDecryptionKey,
`Cipher Id: ${this.id}`,
);
}
break;
case CipherType.SshKey:
if (this.sshKey != null) {
model.sshKey = await this.sshKey.decrypt(userKeyOrOrgKey, `Cipher Id: ${this.id}`);
model.sshKey = await this.sshKey.decrypt(cipherDecryptionKey, `Cipher Id: ${this.id}`);
}
break;
default:
@@ -188,7 +191,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
const attachments: AttachmentView[] = [];
for (const attachment of this.attachments) {
const decryptedAttachment = await attachment.decrypt(
userKeyOrOrgKey,
cipherDecryptionKey,
`Cipher Id: ${this.id}`,
);
attachments.push(decryptedAttachment);
@@ -199,7 +202,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
if (this.fields != null && this.fields.length > 0) {
const fields: FieldView[] = [];
for (const field of this.fields) {
const decryptedField = await field.decrypt(userKeyOrOrgKey);
const decryptedField = await field.decrypt(cipherDecryptionKey);
fields.push(decryptedField);
}
model.fields = fields;
@@ -208,7 +211,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
if (this.passwordHistory != null && this.passwordHistory.length > 0) {
const passwordHistory: PasswordHistoryView[] = [];
for (const ph of this.passwordHistory) {
const decryptedPh = await ph.decrypt(userKeyOrOrgKey);
const decryptedPh = await ph.decrypt(cipherDecryptionKey);
passwordHistory.push(decryptedPh);
}
model.passwordHistory = passwordHistory;