1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-27 18:13:29 +00:00

Merge branch 'main' into SM-1301-getbyidsevent

This commit is contained in:
cd-bitwarden
2025-07-01 11:00:36 -04:00
committed by GitHub
402 changed files with 11624 additions and 1529 deletions

View File

@@ -127,7 +127,6 @@ describe("AuthGuard", () => {
describe("given user is Unlocked", () => {
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
const tests = [
ForceSetPasswordReason.SsoNewJitProvisionedUser,
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
ForceSetPasswordReason.TdeOffboarding,
];
@@ -167,10 +166,6 @@ describe("AuthGuard", () => {
describe("given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
const tests = [
{
reason: ForceSetPasswordReason.SsoNewJitProvisionedUser,
url: "/set-password-jit",
},
{
reason: ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
url: "/set-password",

View File

@@ -39,22 +39,6 @@ export const authGuard: CanActivateFn = async (
return false;
}
if (authStatus === AuthenticationStatus.Locked) {
if (routerState != null) {
messagingService.send("lockedUrl", { url: routerState.url });
}
// TODO PM-9674: when extension refresh is finished, remove promptBiometric
// as it has been integrated into the component as a default feature.
return router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } });
}
if (
!routerState.url.includes("remove-password") &&
(await firstValueFrom(keyConnectorService.convertAccountRequired$))
) {
return router.createUrlTree(["/remove-password"]);
}
const userId = (await firstValueFrom(accountService.activeAccount$)).id;
const forceSetPasswordReason = await firstValueFrom(
masterPasswordService.forceSetPasswordReason$(userId),
@@ -69,12 +53,31 @@ export const authGuard: CanActivateFn = async (
// User JIT provisioned into a master-password-encryption org
if (
authStatus === AuthenticationStatus.Locked &&
forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser &&
!routerState.url.includes("set-password-jit") &&
!routerState.url.includes("set-initial-password")
!routerState.url.includes("set-initial-password") &&
isSetInitialPasswordFlagOn
) {
const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/set-password-jit";
return router.createUrlTree([route]);
return router.createUrlTree(["/set-initial-password"]);
}
if (
authStatus === AuthenticationStatus.Locked &&
forceSetPasswordReason !== ForceSetPasswordReason.SsoNewJitProvisionedUser
) {
if (routerState != null) {
messagingService.send("lockedUrl", { url: routerState.url });
}
// TODO PM-9674: when extension refresh is finished, remove promptBiometric
// as it has been integrated into the component as a default feature.
return router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } });
}
if (
!routerState.url.includes("remove-password") &&
(await firstValueFrom(keyConnectorService.convertAccountRequired$))
) {
return router.createUrlTree(["/remove-password"]);
}
// TDE org user has "manage account recovery" permission
@@ -100,10 +103,10 @@ export const authGuard: CanActivateFn = async (
// Post- Account Recovery or Weak Password on login
if (
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset ||
(forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword &&
!routerState.url.includes("update-temp-password") &&
!routerState.url.includes("change-password"))
(forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset ||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword) &&
!routerState.url.includes("update-temp-password") &&
!routerState.url.includes("change-password")
) {
const route = isChangePasswordFlagOn ? "/change-password" : "/update-temp-password";
return router.createUrlTree([route]);

View File

@@ -0,0 +1,216 @@
# Master Password Management Flows
The Auth Team manages several components that allow a user to either:
1. Set an initial master password
2. Change an existing master password
This document maps all of our password management flows to the components that handle them.
<br>
**Table of Contents**
> - [The Base `InputPasswordComponent`](#the-base-inputpasswordcomponent)
> - [Set Initial Password Flows](#set-initial-password-flows)
> - [Change Password Flows](#change-password-flows)
<br>
**Acronyms**
<ul>
<li>MP = "master password"</li>
<li>MPE = "master password encryption"</li>
<li>TDE = "trusted device encryption"</li>
<li>JIT provision = "just-in-time provision"</li>
</ul>
<br>
## The Base `InputPasswordComponent`
Central to our master password management flows is the base [InputPasswordComponent](https://components.bitwarden.com/?path=/docs/auth-input-password--docs), which is responsible for displaying the appropriate form fields in the UI, performing form validation, and generating appropriate cryptographic properties for each flow. This keeps our UI, validation, and key generation consistent across all master password management flows.
<br>
## Set Initial Password Flows
<table>
<thead>
<tr>
<td><strong>Flow</strong></td>
<td><strong>Route</strong><br><small>(on which user sets MP)</small></td>
<td><strong>Component(s)</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td>
<br>
<strong>Account Registration</strong>
<br><br>
<ol>
<li>Standard Flow</li>
<br>
<li>Self Hosted Flow</li>
<br>
<li>Email Invite Flows <small>(🌐 web only)</small></li>
<br>
</ol>
</td>
<td><code>/finish-signup</code></td>
<td>
<code>RegistrationFinishComponent</code>
<br>
<small>- embeds <code>InputPasswordComponent</code></small>
</tr>
<tr>
<td>
<strong>Trial Initiation</strong> <small>(🌐 web only)</small>
</td>
<td><code>/trial-initiation</code> or<br> <code>/secrets-manager-trial-initiation</code></td>
<td>
<code>CompleteTrialInitiationComponent</code>
<br>
<small>- embeds <code>InputPasswordComponent</code></small>
</td>
</tr>
<tr>
<td>
<br>
<strong>Upon Authentication</strong> (an existing authed user)
<br><br>
<ol>
<li><strong>User JIT provisions<small>*</small> into an MPE org</strong></li>
<br>
<li>
<strong>User JIT provisions<small>*</small> into a TDE org with the "manage account recovery" permission</strong>
<p>That is, the user was given this permission on invitation or by the time they JIT provision.</p>
</li>
<br>
<li>
<strong>TDE user permissions upgraded</strong>
<p>TDE user authenticates after permissions were upgraded to include "manage account recovery".</p>
</li>
<br>
<li>
<strong>TDE offboarding</strong>
<p>User authenticates after their org offboarded from TDE and is now a MPE org.</p>
<p>User must be on a trusted device to set MP, otherwise user must go through Account Recovery.</p>
</li>
</ol>
</td>
<td><code>/set-initial-password</code></td>
<td>
<code>SetInitialPasswordComponent</code>
<br>
<small>- embeds <code>InputPasswordComponent</code></small>
</td>
</tr>
</tbody>
</table>
\* A note on JIT provisioned user flows:
- Even though a JIT provisioned user is a brand-new user who was “just” created, we consider them to be an “existing authed user” _from the perspective of the set initial password flow_. This is because at the time they set their initial password, their account already exists in the database (before setting their password) and they have already authenticated via SSO.
- The same is not true in the _Account Registration_ flows above—that is, during account registration when a user reaches the `/finish-signup` or `/trial-initiation` page to set their initial password, their account does not yet exist in the database, and will only be created once they set an initial password.
<br>
## Change Password Flows
<table>
<thead>
<tr>
<td><strong>Flow</strong></td>
<td><strong>Route</strong><br><small>(on which user changes MP)</small></td>
<td><strong>Component(s)</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td>
<br>
<strong>Account Settings</strong>
(<small><a href="https://bitwarden.com/help/master-password/#change-master-password">Docs</a></small>)
<br>
<small>(🌐 web only)</small>
<br><br>
<p>User changes MP via account settings.</p>
<br>
</td>
<td>
<code>/settings/security/password</code>
<br>(<code>security-routing.module.ts</code>)
</td>
<td>
<code>PasswordSettingsComponent</code>
<br><small>- embeds <code>ChangePasswordComponent</code></small>
<br><small>- embeds <code>InputPasswordComponent</code></small>
</td>
</tr>
<tr>
<td>
<br>
<strong>Upon Authentication</strong>
<br><br>
<ol>
<li>
<strong>Login with non-compliant MP after email accept</strong> <small>(🌐 web only)</small>
<p>User clicks an org email invite link and logs in with their MP that does not meet the orgs policy requirements.</p>
</li>
<br>
<li>
<strong>Login with non-compliant MP</strong>
<p>Existing org user logs in with their MP that does not meet updated org policy requirements.</p>
</li>
<br>
<li>
<strong>Login after Account Recovery</strong>
<p>User logs in after their MP was reset via Account Recovery.</p>
</li>
</ol>
</td>
<td><code>/change-password</code></td>
<td>
<code>ChangePasswordComponent</code>
<br><small>- embeds <code>InputPasswordComponent</code></small>
</td>
</tr>
<tr>
<td>
<br>
<strong>Emergency Access Takeover</strong>
<small>(<a href="https://bitwarden.com/help/emergency-access/">Docs</a>)</small>
<br>
<small>(🌐 web only)</small>
<br><br>
<p>Emergency access Grantee changes the MP for the Grantor.</p>
<br>
</td>
<td>Grantee opens dialog while on <code>/settings/emergency-access</code></td>
<td>
<code>EmergencyAccessTakeoverDialogComponent</code>
<br><small>- embeds <code>InputPasswordComponent</code></small>
</td>
</tr>
<tr>
<td>
<br>
<strong>Account Recovery</strong>
<small>(<a href="https://bitwarden.com/help/account-recovery/">Docs</a>)</small>
<br>
<small>(🌐 web only)</small>
<br><br>
<p>Org member with "manage account recovery" permission changes the MP for another org user via Account Recovery.</p>
<br>
</td>
<td>Org member opens dialog while on <code>/organizations/{org-id}/members</code></td>
<td>
<code>AccountRecoveryDialogComponent</code>
<br><small>- embeds <code>InputPasswordComponent</code></small>
</td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,248 @@
import { firstValueFrom } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import {
OrganizationUserApiService,
OrganizationUserResetPasswordEnrollmentRequest,
} from "@bitwarden/admin-console/common";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management";
import {
SetInitialPasswordService,
SetInitialPasswordCredentials,
SetInitialPasswordUserType,
} from "./set-initial-password.service.abstraction";
export class DefaultSetInitialPasswordService implements SetInitialPasswordService {
constructor(
protected apiService: ApiService,
protected encryptService: EncryptService,
protected i18nService: I18nService,
protected kdfConfigService: KdfConfigService,
protected keyService: KeyService,
protected masterPasswordApiService: MasterPasswordApiService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected organizationUserApiService: OrganizationUserApiService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
) {}
async setInitialPassword(
credentials: SetInitialPasswordCredentials,
userType: SetInitialPasswordUserType,
userId: UserId,
): Promise<void> {
const {
newMasterKey,
newServerMasterKeyHash,
newLocalMasterKeyHash,
newPasswordHint,
kdfConfig,
orgSsoIdentifier,
orgId,
resetPasswordAutoEnroll,
} = credentials;
for (const [key, value] of Object.entries(credentials)) {
if (value == null) {
throw new Error(`${key} not found. Could not set password.`);
}
}
if (userId == null) {
throw new Error("userId not found. Could not set password.");
}
if (userType == null) {
throw new Error("userType not found. Could not set password.");
}
const masterKeyEncryptedUserKey = await this.makeMasterKeyEncryptedUserKey(
newMasterKey,
userId,
);
if (masterKeyEncryptedUserKey == null || !masterKeyEncryptedUserKey[1].encryptedString) {
throw new Error("masterKeyEncryptedUserKey not found. Could not set password.");
}
let keyPair: [string, EncString] | null = null;
let keysRequest: KeysRequest | null = null;
if (userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) {
/**
* A user being JIT provisioned into a MP encryption org does not yet have a user
* asymmetric key pair, so we create it for them here.
*
* Sidenote:
* In the case of a TDE user whose permissions require that they have a MP - that user
* will already have a user asymmetric key pair by this point, so we skip this if-block
* so that we don't create a new key pair for them.
*/
// Extra safety check (see description on https://github.com/bitwarden/clients/pull/10180):
// In case we have have a local private key and are not sure whether it has been posted to the server,
// we post the local private key instead of generating a new one
const existingUserPrivateKey = (await firstValueFrom(
this.keyService.userPrivateKey$(userId),
)) as Uint8Array;
const existingUserPublicKey = await firstValueFrom(this.keyService.userPublicKey$(userId));
if (existingUserPrivateKey != null && existingUserPublicKey != null) {
const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey);
// Existing key pair
keyPair = [
existingUserPublicKeyB64,
await this.encryptService.wrapDecapsulationKey(
existingUserPrivateKey,
masterKeyEncryptedUserKey[0],
),
];
} else {
// New key pair
keyPair = await this.keyService.makeKeyPair(masterKeyEncryptedUserKey[0]);
}
if (keyPair == null) {
throw new Error("keyPair not found. Could not set password.");
}
if (!keyPair[1].encryptedString) {
throw new Error("encrypted private key not found. Could not set password.");
}
keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
}
const request = new SetPasswordRequest(
newServerMasterKeyHash,
masterKeyEncryptedUserKey[1].encryptedString,
newPasswordHint,
orgSsoIdentifier,
keysRequest,
kdfConfig.kdfType,
kdfConfig.iterations,
);
await this.masterPasswordApiService.setPassword(request);
// Clear force set password reason to allow navigation back to vault.
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
// User now has a password so update account decryption options in state
await this.updateAccountDecryptionProperties(
newMasterKey,
kdfConfig,
masterKeyEncryptedUserKey,
userId,
);
/**
* Set the private key only for new JIT provisioned users in MP encryption orgs.
* (Existing TDE users will have their private key set on sync or on login.)
*/
if (keyPair != null && userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) {
if (!keyPair[1].encryptedString) {
throw new Error("encrypted private key not found. Could not set private key in state.");
}
await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
}
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
if (resetPasswordAutoEnroll) {
await this.handleResetPasswordAutoEnroll(newServerMasterKeyHash, orgId, userId);
}
}
private async makeMasterKeyEncryptedUserKey(
masterKey: MasterKey,
userId: UserId,
): Promise<[UserKey, EncString]> {
let masterKeyEncryptedUserKey: [UserKey, EncString] | null = null;
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
if (userKey == null) {
masterKeyEncryptedUserKey = await this.keyService.makeUserKey(masterKey);
} else {
masterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey);
}
return masterKeyEncryptedUserKey;
}
private async updateAccountDecryptionProperties(
masterKey: MasterKey,
kdfConfig: KdfConfig,
masterKeyEncryptedUserKey: [UserKey, EncString],
userId: UserId,
) {
const userDecryptionOpts = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
userDecryptionOpts.hasMasterPassword = true;
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
await this.masterPasswordService.setMasterKey(masterKey, userId);
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
}
private async handleResetPasswordAutoEnroll(
masterKeyHash: string,
orgId: string,
userId: UserId,
) {
const organizationKeys = await this.organizationApiService.getKeys(orgId);
if (organizationKeys == null) {
throw new Error(
"Organization keys response is null. Could not handle reset password auto enroll.",
);
}
const orgPublicKey = Utils.fromB64ToArray(organizationKeys.publicKey);
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
if (userKey == null) {
throw new Error("userKey not found. Could not handle reset password auto enroll.");
}
// RSA encrypt user key with organization public key
const orgPublicKeyEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
userKey,
orgPublicKey,
);
if (orgPublicKeyEncryptedUserKey == null || !orgPublicKeyEncryptedUserKey.encryptedString) {
throw new Error(
"orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.",
);
}
const enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest();
enrollmentRequest.masterPasswordHash = masterKeyHash;
enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString;
await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
orgId,
userId,
enrollmentRequest,
);
}
}

View File

@@ -0,0 +1,633 @@
import { MockProxy, mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import {
OrganizationUserApiService,
OrganizationUserResetPasswordEnrollmentRequest,
} from "@bitwarden/admin-console/common";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import {
FakeUserDecryptionOptions as UserDecryptionOptions,
InternalUserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey, UserPrivateKey, UserPublicKey } from "@bitwarden/common/types/key";
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
import { DefaultSetInitialPasswordService } from "./default-set-initial-password.service.implementation";
import {
SetInitialPasswordCredentials,
SetInitialPasswordService,
SetInitialPasswordUserType,
} from "./set-initial-password.service.abstraction";
describe("DefaultSetInitialPasswordService", () => {
let sut: SetInitialPasswordService;
let apiService: MockProxy<ApiService>;
let encryptService: MockProxy<EncryptService>;
let i18nService: MockProxy<I18nService>;
let kdfConfigService: MockProxy<KdfConfigService>;
let keyService: MockProxy<KeyService>;
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
beforeEach(() => {
apiService = mock<ApiService>();
encryptService = mock<EncryptService>();
i18nService = mock<I18nService>();
kdfConfigService = mock<KdfConfigService>();
keyService = mock<KeyService>();
masterPasswordApiService = mock<MasterPasswordApiService>();
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
organizationApiService = mock<OrganizationApiServiceAbstraction>();
organizationUserApiService = mock<OrganizationUserApiService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
sut = new DefaultSetInitialPasswordService(
apiService,
encryptService,
i18nService,
kdfConfigService,
keyService,
masterPasswordApiService,
masterPasswordService,
organizationApiService,
organizationUserApiService,
userDecryptionOptionsService,
);
});
it("should instantiate", () => {
expect(sut).not.toBeFalsy();
});
describe("setInitialPassword(...)", () => {
// Mock function parameters
let credentials: SetInitialPasswordCredentials;
let userType: SetInitialPasswordUserType;
let userId: UserId;
// Mock other function data
let userKey: UserKey;
let userKeyEncString: EncString;
let masterKeyEncryptedUserKey: [UserKey, EncString];
let existingUserPublicKey: UserPublicKey;
let existingUserPrivateKey: UserPrivateKey;
let userKeyEncryptedPrivateKey: EncString;
let keyPair: [string, EncString];
let keysRequest: KeysRequest;
let organizationKeys: OrganizationKeysResponse;
let orgPublicKeyEncryptedUserKey: EncString;
let userDecryptionOptions: UserDecryptionOptions;
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
let setPasswordRequest: SetPasswordRequest;
let enrollmentRequest: OrganizationUserResetPasswordEnrollmentRequest;
beforeEach(() => {
// Mock function parameters
credentials = {
newMasterKey: new SymmetricCryptoKey(new Uint8Array(32).buffer as CsprngArray) as MasterKey,
newServerMasterKeyHash: "newServerMasterKeyHash",
newLocalMasterKeyHash: "newLocalMasterKeyHash",
newPasswordHint: "newPasswordHint",
kdfConfig: DEFAULT_KDF_CONFIG,
orgSsoIdentifier: "orgSsoIdentifier",
orgId: "orgId",
resetPasswordAutoEnroll: false,
};
userId = "userId" as UserId;
userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
// Mock other function data
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
userKeyEncString = new EncString("masterKeyEncryptedUserKey");
masterKeyEncryptedUserKey = [userKey, userKeyEncString];
existingUserPublicKey = Utils.fromB64ToArray("existingUserPublicKey") as UserPublicKey;
existingUserPrivateKey = Utils.fromB64ToArray("existingUserPrivateKey") as UserPrivateKey;
userKeyEncryptedPrivateKey = new EncString("userKeyEncryptedPrivateKey");
keyPair = ["publicKey", new EncString("privateKey")];
keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
organizationKeys = {
privateKey: "orgPrivateKey",
publicKey: "orgPublicKey",
} as OrganizationKeysResponse;
orgPublicKeyEncryptedUserKey = new EncString("orgPublicKeyEncryptedUserKey");
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
setPasswordRequest = new SetPasswordRequest(
credentials.newServerMasterKeyHash,
masterKeyEncryptedUserKey[1].encryptedString,
credentials.newPasswordHint,
credentials.orgSsoIdentifier,
keysRequest,
credentials.kdfConfig.kdfType,
credentials.kdfConfig.iterations,
);
enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest();
enrollmentRequest.masterPasswordHash = credentials.newServerMasterKeyHash;
enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString;
});
interface MockConfig {
userType: SetInitialPasswordUserType;
userHasUserKey: boolean;
userHasLocalKeyPair: boolean;
resetPasswordAutoEnroll: boolean;
}
const defaultMockConfig: MockConfig = {
userType: SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER,
userHasUserKey: true,
userHasLocalKeyPair: false,
resetPasswordAutoEnroll: false,
};
function setupMocks(config: MockConfig = defaultMockConfig) {
// Mock makeMasterKeyEncryptedUserKey() values
if (config.userHasUserKey) {
keyService.userKey$.mockReturnValue(of(userKey));
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(masterKeyEncryptedUserKey);
} else {
keyService.userKey$.mockReturnValue(of(null));
keyService.makeUserKey.mockResolvedValue(masterKeyEncryptedUserKey);
}
// Mock keyPair values
if (config.userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) {
if (config.userHasLocalKeyPair) {
keyService.userPrivateKey$.mockReturnValue(of(existingUserPrivateKey));
keyService.userPublicKey$.mockReturnValue(of(existingUserPublicKey));
encryptService.wrapDecapsulationKey.mockResolvedValue(userKeyEncryptedPrivateKey);
} else {
keyService.userPrivateKey$.mockReturnValue(of(null));
keyService.userPublicKey$.mockReturnValue(of(null));
keyService.makeKeyPair.mockResolvedValue(keyPair);
}
}
// Mock handleResetPasswordAutoEnroll() values
if (config.resetPasswordAutoEnroll) {
organizationApiService.getKeys.mockResolvedValue(organizationKeys);
encryptService.encapsulateKeyUnsigned.mockResolvedValue(orgPublicKeyEncryptedUserKey);
keyService.userKey$.mockReturnValue(of(userKey));
}
}
describe("general error handling", () => {
[
"newMasterKey",
"newServerMasterKeyHash",
"newLocalMasterKeyHash",
"newPasswordHint",
"kdfConfig",
"orgSsoIdentifier",
"orgId",
"resetPasswordAutoEnroll",
].forEach((key) => {
it(`should throw if ${key} is not provided on the SetInitialPasswordCredentials object`, async () => {
// Arrange
const invalidCredentials: SetInitialPasswordCredentials = {
...credentials,
[key]: null,
};
// Act
const promise = sut.setInitialPassword(invalidCredentials, userType, userId);
// Assert
await expect(promise).rejects.toThrow(`${key} not found. Could not set password.`);
});
});
["userId", "userType"].forEach((param) => {
it(`should throw if ${param} was not passed in`, async () => {
// Arrange & Act
const promise = sut.setInitialPassword(
credentials,
param === "userType" ? null : userType,
param === "userId" ? null : userId,
);
// Assert
await expect(promise).rejects.toThrow(`${param} not found. Could not set password.`);
});
});
});
describe("given SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER", () => {
beforeEach(() => {
userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
});
describe("given the user has an existing local key pair", () => {
it("should NOT create a brand new key pair for the user", async () => {
// Arrange
setPasswordRequest.keys = {
encryptedPrivateKey: userKeyEncryptedPrivateKey.encryptedString,
publicKey: Utils.fromBufferToB64(existingUserPublicKey),
};
setupMocks({ ...defaultMockConfig, userHasLocalKeyPair: true });
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(keyService.userPrivateKey$).toHaveBeenCalledWith(userId);
expect(keyService.userPublicKey$).toHaveBeenCalledWith(userId);
expect(encryptService.wrapDecapsulationKey).toHaveBeenCalledWith(
existingUserPrivateKey,
masterKeyEncryptedUserKey[0],
);
expect(keyService.makeKeyPair).not.toHaveBeenCalled();
});
});
describe("given the user has a userKey", () => {
it("should successfully set an initial password", async () => {
// Arrange
setupMocks();
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
});
});
describe("given the user does NOT have a userKey", () => {
it("should successfully set an initial password", async () => {
// Arrange
setupMocks({ ...defaultMockConfig, userHasUserKey: false });
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
});
});
it("should throw if a key pair is not found", async () => {
// Arrange
keyPair = null;
setupMocks();
// Act
const promise = sut.setInitialPassword(credentials, userType, userId);
// Assert
await expect(promise).rejects.toThrow("keyPair not found. Could not set password.");
expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled();
});
it("should throw if an encrypted private key is not found", async () => {
// Arrange
keyPair[1].encryptedString = "" as EncryptedString;
setupMocks();
// Act
const promise = sut.setInitialPassword(credentials, userType, userId);
// Assert
await expect(promise).rejects.toThrow(
"encrypted private key not found. Could not set password.",
);
expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled();
});
describe("given the initial password has been successfully set", () => {
it("should clear the ForceSetPasswordReason by setting it to None", async () => {
// Arrange
setupMocks();
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith(
ForceSetPasswordReason.None,
userId,
);
});
it("should update account decryption properties", async () => {
// Arrange
setupMocks();
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
userDecryptionOptions,
);
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
expect(masterPasswordService.setMasterKey).toHaveBeenCalledWith(
credentials.newMasterKey,
userId,
);
expect(keyService.setUserKey).toHaveBeenCalledWith(masterKeyEncryptedUserKey[0], userId);
});
it("should set the private key to state", async () => {
// Arrange
setupMocks();
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(keyService.setPrivateKey).toHaveBeenCalledWith(keyPair[1].encryptedString, userId);
});
it("should set the local master key hash to state", async () => {
// Arrange
setupMocks();
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith(
credentials.newLocalMasterKeyHash,
userId,
);
});
describe("given resetPasswordAutoEnroll is true", () => {
it(`should handle reset password (account recovery) auto enroll`, async () => {
// Arrange
credentials.resetPasswordAutoEnroll = true;
setupMocks({ ...defaultMockConfig, resetPasswordAutoEnroll: true });
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
).toHaveBeenCalledWith(credentials.orgId, userId, enrollmentRequest);
});
it("should throw if organization keys are not found", async () => {
// Arrange
credentials.resetPasswordAutoEnroll = true;
organizationKeys = null;
setupMocks({ ...defaultMockConfig, resetPasswordAutoEnroll: true });
// Act
const promise = sut.setInitialPassword(credentials, userType, userId);
// Assert
await expect(promise).rejects.toThrow(
"Organization keys response is null. Could not handle reset password auto enroll.",
);
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
).not.toHaveBeenCalled();
});
["orgPublicKeyEncryptedUserKey", "orgPublicKeyEncryptedUserKey.encryptedString"].forEach(
(property) => {
it("should throw if orgPublicKeyEncryptedUserKey is not found", async () => {
// Arrange
credentials.resetPasswordAutoEnroll = true;
if (property === "orgPublicKeyEncryptedUserKey") {
orgPublicKeyEncryptedUserKey = null;
} else {
orgPublicKeyEncryptedUserKey.encryptedString = "" as EncryptedString;
}
setupMocks({ ...defaultMockConfig, resetPasswordAutoEnroll: true });
// Act
const promise = sut.setInitialPassword(credentials, userType, userId);
// Assert
await expect(promise).rejects.toThrow(
"orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.",
);
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(
setPasswordRequest,
);
expect(
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
).not.toHaveBeenCalled();
});
},
);
});
describe("given resetPasswordAutoEnroll is false", () => {
it(`should NOT handle reset password (account recovery) auto enroll`, async () => {
// Arrange
credentials.resetPasswordAutoEnroll = false;
setupMocks();
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
).not.toHaveBeenCalled();
});
});
});
});
describe("given SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP", () => {
beforeEach(() => {
userType = SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP;
setPasswordRequest.keys = null;
});
it("should NOT generate a keyPair", async () => {
// Arrange
setupMocks({ ...defaultMockConfig, userType });
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(keyService.userPrivateKey$).not.toHaveBeenCalled();
expect(keyService.userPublicKey$).not.toHaveBeenCalled();
expect(encryptService.wrapDecapsulationKey).not.toHaveBeenCalled();
expect(keyService.makeKeyPair).not.toHaveBeenCalled();
});
describe("given the user has a userKey", () => {
it("should successfully set an initial password", async () => {
// Arrange
setupMocks({ ...defaultMockConfig, userType });
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
});
});
describe("given the user does NOT have a userKey", () => {
it("should successfully set an initial password", async () => {
// Arrange
setupMocks({ ...defaultMockConfig, userType });
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
});
});
describe("given the initial password has been successfully set", () => {
it("should clear the ForceSetPasswordReason by setting it to None", async () => {
// Arrange
setupMocks({ ...defaultMockConfig, userType });
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith(
ForceSetPasswordReason.None,
userId,
);
});
it("should update account decryption properties", async () => {
// Arrange
setupMocks({ ...defaultMockConfig, userType });
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
userDecryptionOptions,
);
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
expect(masterPasswordService.setMasterKey).toHaveBeenCalledWith(
credentials.newMasterKey,
userId,
);
expect(keyService.setUserKey).toHaveBeenCalledWith(masterKeyEncryptedUserKey[0], userId);
});
it("should NOT set the private key to state", async () => {
// Arrange
setupMocks({ ...defaultMockConfig, userType });
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
});
it("should set the local master key hash to state", async () => {
// Arrange
setupMocks({ ...defaultMockConfig, userType });
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith(
credentials.newLocalMasterKeyHash,
userId,
);
});
describe("given resetPasswordAutoEnroll is true", () => {
it(`should handle reset password (account recovery) auto enroll`, async () => {
// Arrange
credentials.resetPasswordAutoEnroll = true;
setupMocks({ ...defaultMockConfig, userType, resetPasswordAutoEnroll: true });
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
).toHaveBeenCalledWith(credentials.orgId, userId, enrollmentRequest);
});
});
describe("given resetPasswordAutoEnroll is false", () => {
it(`should NOT handle reset password (account recovery) auto enroll`, async () => {
// Arrange
setupMocks({ ...defaultMockConfig, userType });
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
).not.toHaveBeenCalled();
});
});
});
});
});
});

View File

@@ -0,0 +1,29 @@
@if (initializing) {
<div class="tw-flex tw-items-center tw-justify-center">
<i
class="bwi bwi-spinner bwi-spin bwi-3x"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</div>
} @else {
<bit-callout
*ngIf="resetPasswordAutoEnroll"
type="warning"
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</bit-callout>
<auth-input-password
[flow]="inputPasswordFlow"
[email]="email"
[userId]="userId"
[loading]="submitting"
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
[primaryButtonText]="{ key: 'createAccount' }"
[secondaryButtonText]="{ key: 'logOut' }"
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
(onSecondaryButtonClick)="logout()"
></auth-input-password>
}

View File

@@ -0,0 +1,249 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import {
InputPasswordComponent,
InputPasswordFlow,
PasswordInputResult,
} from "@bitwarden/auth/angular";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
import {
AnonLayoutWrapperDataService,
CalloutComponent,
DialogService,
ToastService,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import {
SetInitialPasswordCredentials,
SetInitialPasswordService,
SetInitialPasswordUserType,
} from "./set-initial-password.service.abstraction";
@Component({
standalone: true,
templateUrl: "set-initial-password.component.html",
imports: [CalloutComponent, CommonModule, InputPasswordComponent, I18nPipe],
})
export class SetInitialPasswordComponent implements OnInit {
protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser;
protected email?: string;
protected forceSetPasswordReason?: ForceSetPasswordReason;
protected initializing = true;
protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
protected orgId?: string;
protected orgSsoIdentifier?: string;
protected resetPasswordAutoEnroll?: boolean;
protected submitting = false;
protected userId?: UserId;
protected userType?: SetInitialPasswordUserType;
constructor(
private accountService: AccountService,
private activatedRoute: ActivatedRoute,
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private dialogService: DialogService,
private i18nService: I18nService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private messagingService: MessagingService,
private organizationApiService: OrganizationApiServiceAbstraction,
private policyApiService: PolicyApiServiceAbstraction,
private router: Router,
private setInitialPasswordService: SetInitialPasswordService,
private ssoLoginService: SsoLoginServiceAbstraction,
private syncService: SyncService,
private toastService: ToastService,
private validationService: ValidationService,
) {}
async ngOnInit() {
await this.syncService.fullSync(true);
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
this.userId = activeAccount?.id;
this.email = activeAccount?.email;
await this.determineUserType();
await this.handleQueryParams();
this.initializing = false;
}
private async determineUserType() {
if (!this.userId) {
throw new Error("userId not found. Could not determine user type.");
}
this.forceSetPasswordReason = await firstValueFrom(
this.masterPasswordService.forceSetPasswordReason$(this.userId),
);
if (
this.forceSetPasswordReason ===
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
) {
this.userType = SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP;
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: { key: "setMasterPassword" },
pageSubtitle: { key: "orgPermissionsUpdatedMustSetPassword" },
});
} else {
this.userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: { key: "joinOrganization" },
pageSubtitle: { key: "finishJoiningThisOrganizationBySettingAMasterPassword" },
});
}
}
private async handleQueryParams() {
if (!this.userId) {
throw new Error("userId not found. Could not handle query params.");
}
const qParams = await firstValueFrom(this.activatedRoute.queryParams);
this.orgSsoIdentifier =
qParams.identifier ??
(await this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.userId));
if (this.orgSsoIdentifier != null) {
try {
const autoEnrollStatus = await this.organizationApiService.getAutoEnrollStatus(
this.orgSsoIdentifier,
);
this.orgId = autoEnrollStatus.id;
this.resetPasswordAutoEnroll = autoEnrollStatus.resetPasswordEnabled;
this.masterPasswordPolicyOptions =
await this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(this.orgId);
} catch {
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("errorOccurred"),
});
}
}
}
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
this.submitting = true;
if (!passwordInputResult.newMasterKey) {
throw new Error("newMasterKey not found. Could not set initial password.");
}
if (!passwordInputResult.newServerMasterKeyHash) {
throw new Error("newServerMasterKeyHash not found. Could not set initial password.");
}
if (!passwordInputResult.newLocalMasterKeyHash) {
throw new Error("newLocalMasterKeyHash not found. Could not set initial password.");
}
// newPasswordHint can have an empty string as a valid value, so we specifically check for null or undefined
if (passwordInputResult.newPasswordHint == null) {
throw new Error("newPasswordHint not found. Could not set initial password.");
}
if (!passwordInputResult.kdfConfig) {
throw new Error("kdfConfig not found. Could not set initial password.");
}
if (!this.userId) {
throw new Error("userId not found. Could not set initial password.");
}
if (!this.userType) {
throw new Error("userType not found. Could not set initial password.");
}
if (!this.orgSsoIdentifier) {
throw new Error("orgSsoIdentifier not found. Could not set initial password.");
}
if (!this.orgId) {
throw new Error("orgId not found. Could not set initial password.");
}
// resetPasswordAutoEnroll can have `false` as a valid value, so we specifically check for null or undefined
if (this.resetPasswordAutoEnroll == null) {
throw new Error("resetPasswordAutoEnroll not found. Could not set initial password.");
}
try {
const credentials: SetInitialPasswordCredentials = {
newMasterKey: passwordInputResult.newMasterKey,
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
newPasswordHint: passwordInputResult.newPasswordHint,
kdfConfig: passwordInputResult.kdfConfig,
orgSsoIdentifier: this.orgSsoIdentifier,
orgId: this.orgId,
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
};
await this.setInitialPasswordService.setInitialPassword(
credentials,
this.userType,
this.userId,
);
this.showSuccessToastByUserType();
this.submitting = false;
await this.router.navigate(["vault"]);
} catch (e) {
this.validationService.showError(e);
this.submitting = false;
}
}
private showSuccessToastByUserType() {
if (this.userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) {
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("accountSuccessfullyCreated"),
});
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("inviteAccepted"),
});
}
if (
this.userType ===
SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP
) {
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("masterPasswordSuccessfullySet"),
});
}
}
protected async logout() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "logOut" },
content: { key: "logOutConfirmation" },
acceptButtonText: { key: "logOut" },
type: "warning",
});
if (confirmed) {
this.messagingService.send("logout");
}
}
}

View File

@@ -0,0 +1,64 @@
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey } from "@bitwarden/common/types/key";
import { KdfConfig } from "@bitwarden/key-management";
export const _SetInitialPasswordUserType = {
/**
* A user being "just-in-time" (JIT) provisioned into a master-password-encryption org
*/
JIT_PROVISIONED_MP_ORG_USER: "jit_provisioned_mp_org_user",
/**
* Could be one of two scenarios:
* 1. A user being "just-in-time" (JIT) provisioned into a trusted-device-encryption org
* with the reset password permission granted ("manage account recovery"), which requires
* that the user sets a master password
* 2. An user in a trusted-device-encryption org whose permissions were upgraded to include
* the reset password permission ("manage account recovery"), which requires that the user
* sets a master password
*/
TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP:
"tde_org_user_reset_password_permission_requires_mp",
} as const;
type _SetInitialPasswordUserType = typeof _SetInitialPasswordUserType;
export type SetInitialPasswordUserType =
_SetInitialPasswordUserType[keyof _SetInitialPasswordUserType];
export const SetInitialPasswordUserType: Readonly<{
[K in keyof typeof _SetInitialPasswordUserType]: SetInitialPasswordUserType;
}> = Object.freeze(_SetInitialPasswordUserType);
export interface SetInitialPasswordCredentials {
newMasterKey: MasterKey;
newServerMasterKeyHash: string;
newLocalMasterKeyHash: string;
newPasswordHint: string;
kdfConfig: KdfConfig;
orgSsoIdentifier: string;
orgId: string;
resetPasswordAutoEnroll: boolean;
}
/**
* Handles setting an initial password for an existing authed user.
*
* To see the different scenarios where an existing authed user needs to set an
* initial password, see {@link SetInitialPasswordUserType}
*/
export abstract class SetInitialPasswordService {
/**
* Sets an initial password for an existing authed user who is either:
* - {@link SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER}
* - {@link SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP}
*
* @param credentials An object of the credentials needed to set the initial password
* @throws If any property on the `credentials` object is null or undefined, or if a
* masterKeyEncryptedUserKey or newKeyPair could not be created.
*/
abstract setInitialPassword: (
credentials: SetInitialPasswordCredentials,
userType: SetInitialPasswordUserType,
userId: UserId,
) => Promise<void>;
}

View File

@@ -35,7 +35,7 @@ import {
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import {
AuthRequestApiService,
AuthRequestApiServiceAbstraction,
AuthRequestService,
AuthRequestServiceAbstraction,
DefaultAuthRequestApiService,
@@ -59,7 +59,6 @@ import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstracti
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import {
InternalOrganizationServiceAbstraction,
@@ -257,7 +256,6 @@ import { ApiService } from "@bitwarden/common/services/api.service";
import { AuditService } from "@bitwarden/common/services/audit.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
import { SearchService } from "@bitwarden/common/services/search.service";
import {
PasswordStrengthService,
PasswordStrengthServiceAbstraction,
@@ -279,6 +277,7 @@ import {
FolderService as FolderServiceAbstraction,
InternalFolderService,
} from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/vault/abstractions/search.service";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import {
@@ -295,6 +294,7 @@ import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-u
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { SearchService } from "@bitwarden/common/vault/services/search.service";
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service";
import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks";
@@ -339,6 +339,8 @@ import {
VaultExportServiceAbstraction,
} from "@bitwarden/vault-export-core";
import { DefaultSetInitialPasswordService } from "../auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
import { SetInitialPasswordService } from "../auth/password-management/set-initial-password/set-initial-password.service.abstraction";
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
@@ -1181,6 +1183,11 @@ const safeProviders: SafeProvider[] = [
useClass: DevicesServiceImplementation,
deps: [DevicesApiServiceAbstraction, AppIdServiceAbstraction],
}),
safeProvider({
provide: AuthRequestApiServiceAbstraction,
useClass: DefaultAuthRequestApiService,
deps: [ApiServiceAbstraction, LogService],
}),
safeProvider({
provide: DeviceTrustServiceAbstraction,
useClass: DeviceTrustService,
@@ -1205,12 +1212,12 @@ const safeProviders: SafeProvider[] = [
useClass: AuthRequestService,
deps: [
AppIdServiceAbstraction,
AccountServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
KeyService,
EncryptService,
ApiServiceAbstraction,
StateProvider,
AuthRequestApiServiceAbstraction,
],
}),
safeProvider({
@@ -1414,6 +1421,22 @@ const safeProviders: SafeProvider[] = [
InternalUserDecryptionOptionsServiceAbstraction,
],
}),
safeProvider({
provide: SetInitialPasswordService,
useClass: DefaultSetInitialPasswordService,
deps: [
ApiServiceAbstraction,
EncryptService,
I18nServiceAbstraction,
KdfConfigService,
KeyService,
MasterPasswordApiServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
OrganizationApiServiceAbstraction,
OrganizationUserApiService,
InternalUserDecryptionOptionsServiceAbstraction,
],
}),
safeProvider({
provide: DefaultServerSettingsService,
useClass: DefaultServerSettingsService,
@@ -1477,11 +1500,6 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultCipherAuthorizationService,
deps: [CollectionService, OrganizationServiceAbstraction, AccountServiceAbstraction],
}),
safeProvider({
provide: AuthRequestApiService,
useClass: DefaultAuthRequestApiService,
deps: [ApiServiceAbstraction, LogService],
}),
safeProvider({
provide: LoginApprovalComponentServiceAbstraction,
useClass: DefaultLoginApprovalComponentService,

View File

@@ -12,7 +12,6 @@ import {
combineLatest,
} from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -25,6 +24,7 @@ import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { DialogService, ToastService } from "@bitwarden/components";
@Directive()

View File

@@ -6,6 +6,7 @@ import { Observable } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { CollectionView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { UserId } from "@bitwarden/common/types/guid";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { DynamicTreeNode } from "../vault-filter/models/dynamic-tree-node.model";
@@ -14,11 +15,14 @@ import { DynamicTreeNode } from "../vault-filter/models/dynamic-tree-node.model"
* @deprecated August 30 2022: Use new VaultFilterService with observables
*/
export abstract class DeprecatedVaultFilterService {
buildOrganizations: () => Promise<Organization[]>;
buildNestedFolders: (organizationId?: string) => Observable<DynamicTreeNode<FolderView>>;
buildCollections: (organizationId?: string) => Promise<DynamicTreeNode<CollectionView>>;
buildCollapsedFilterNodes: () => Promise<Set<string>>;
storeCollapsedFilterNodes: (collapsedFilterNodes: Set<string>) => Promise<void>;
checkForSingleOrganizationPolicy: () => Promise<boolean>;
checkForOrganizationDataOwnershipPolicy: () => Promise<boolean>;
abstract buildOrganizations(): Promise<Organization[]>;
abstract buildNestedFolders(organizationId?: string): Observable<DynamicTreeNode<FolderView>>;
abstract buildCollections(organizationId?: string): Promise<DynamicTreeNode<CollectionView>>;
abstract buildCollapsedFilterNodes(userId: UserId): Promise<Set<string>>;
abstract storeCollapsedFilterNodes(
collapsedFilterNodes: Set<string>,
userId: UserId,
): Promise<void>;
abstract checkForSingleOrganizationPolicy(): Promise<boolean>;
abstract checkForOrganizationDataOwnershipPolicy(): Promise<boolean>;
}

View File

@@ -15,11 +15,11 @@ import {
takeUntil,
} from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";

View File

@@ -7,6 +7,9 @@ import { firstValueFrom, Observable } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { CollectionView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { UserId } from "@bitwarden/common/types/guid";
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
@@ -29,6 +32,8 @@ export class VaultFilterComponent implements OnInit {
@Output() onAddFolder = new EventEmitter<never>();
@Output() onEditFolder = new EventEmitter<FolderView>();
private activeUserId: UserId;
isLoaded = false;
collapsedFilterNodes: Set<string>;
organizations: Organization[];
@@ -37,14 +42,20 @@ export class VaultFilterComponent implements OnInit {
collections: DynamicTreeNode<CollectionView>;
folders$: Observable<DynamicTreeNode<FolderView>>;
constructor(protected vaultFilterService: DeprecatedVaultFilterService) {}
constructor(
protected vaultFilterService: DeprecatedVaultFilterService,
protected accountService: AccountService,
) {}
get displayCollections() {
return this.collections?.fullList != null && this.collections.fullList.length > 0;
}
async ngOnInit(): Promise<void> {
this.collapsedFilterNodes = await this.vaultFilterService.buildCollapsedFilterNodes();
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.collapsedFilterNodes = await this.vaultFilterService.buildCollapsedFilterNodes(
this.activeUserId,
);
this.organizations = await this.vaultFilterService.buildOrganizations();
if (this.organizations != null && this.organizations.length > 0) {
this.activeOrganizationDataOwnershipPolicy =
@@ -68,7 +79,10 @@ export class VaultFilterComponent implements OnInit {
} else {
this.collapsedFilterNodes.add(node.id);
}
await this.vaultFilterService.storeCollapsedFilterNodes(this.collapsedFilterNodes);
await this.vaultFilterService.storeCollapsedFilterNodes(
this.collapsedFilterNodes,
this.activeUserId,
);
}
async applyFilter(filter: VaultFilter) {

View File

@@ -12,7 +12,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state";
import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
@@ -28,10 +28,9 @@ const NestingDelimiter = "/";
@Injectable()
export class VaultFilterService implements DeprecatedVaultFilterServiceAbstraction {
private collapsedGroupingsState: ActiveUserState<string[]> =
this.stateProvider.getActive(COLLAPSED_GROUPINGS);
private readonly collapsedGroupings$: Observable<Set<string>> =
this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c)));
private collapsedGroupingsState(userId: UserId): SingleUserState<string[]> {
return this.stateProvider.getUser(userId, COLLAPSED_GROUPINGS);
}
constructor(
protected organizationService: OrganizationService,
@@ -43,12 +42,17 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
protected accountService: AccountService,
) {}
async storeCollapsedFilterNodes(collapsedFilterNodes: Set<string>): Promise<void> {
await this.collapsedGroupingsState.update(() => Array.from(collapsedFilterNodes));
async storeCollapsedFilterNodes(
collapsedFilterNodes: Set<string>,
userId: UserId,
): Promise<void> {
await this.collapsedGroupingsState(userId).update(() => Array.from(collapsedFilterNodes));
}
async buildCollapsedFilterNodes(): Promise<Set<string>> {
return await firstValueFrom(this.collapsedGroupings$);
async buildCollapsedFilterNodes(userId: UserId): Promise<Set<string>> {
return await firstValueFrom(
this.collapsedGroupingsState(userId).state$.pipe(map((c) => new Set(c))),
);
}
async buildOrganizations(): Promise<Organization[]> {

View File

@@ -261,7 +261,7 @@ export class InputPasswordComponent implements OnInit {
}
}
submit = async () => {
submit = async (): Promise<PasswordInputResult | undefined> => {
try {
this.isSubmitting.emit(true);
@@ -280,8 +280,7 @@ export class InputPasswordComponent implements OnInit {
const checkForBreaches = this.formGroup.controls.checkForBreaches?.value ?? true;
if (this.flow === InputPasswordFlow.ChangePasswordDelegation) {
await this.handleChangePasswordDelegationFlow(newPassword);
return;
return await this.handleChangePasswordDelegationFlow(newPassword);
}
if (!this.email) {
@@ -388,6 +387,7 @@ export class InputPasswordComponent implements OnInit {
// 5. Emit cryptographic keys and other password related properties
this.onPasswordFormSubmit.emit(passwordInputResult);
return passwordInputResult;
} catch (e) {
this.validationService.showError(e);
} finally {
@@ -441,7 +441,9 @@ export class InputPasswordComponent implements OnInit {
}
}
private async handleChangePasswordDelegationFlow(newPassword: string) {
private async handleChangePasswordDelegationFlow(
newPassword: string,
): Promise<PasswordInputResult | undefined> {
const newPasswordVerified = await this.verifyNewPassword(
newPassword,
this.passwordStrengthScore,
@@ -456,6 +458,7 @@ export class InputPasswordComponent implements OnInit {
};
this.onPasswordFormSubmit.emit(passwordInputResult);
return passwordInputResult;
}
/**

View File

@@ -39,7 +39,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { ButtonModule, LinkModule, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { AuthRequestApiService } from "../../common/abstractions/auth-request-api.service";
import { AuthRequestApiServiceAbstraction } from "../../common/abstractions/auth-request-api.service";
import { LoginViaAuthRequestCacheService } from "../../common/services/auth-request/default-login-via-auth-request-cache.service";
// FIXME: update to use a const object instead of a typescript enum
@@ -85,7 +85,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
private accountService: AccountService,
private anonymousHubService: AnonymousHubService,
private appIdService: AppIdService,
private authRequestApiService: AuthRequestApiService,
private authRequestApiService: AuthRequestApiServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
private authService: AuthService,
private cryptoFunctionService: CryptoFunctionService,

View File

@@ -23,10 +23,12 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -116,6 +118,7 @@ export class SsoComponent implements OnInit {
private toastService: ToastService,
private ssoComponentService: SsoComponentService,
private loginSuccessHandlerService: LoginSuccessHandlerService,
private configService: ConfigService,
) {
environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html";
@@ -531,7 +534,12 @@ export class SsoComponent implements OnInit {
}
private async handleChangePasswordRequired(orgIdentifier: string) {
await this.router.navigate(["set-password-jit"], {
const isSetInitialPasswordRefactorFlagOn = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_SetInitialPasswordRefactor,
);
const route = isSetInitialPasswordRefactorFlagOn ? "set-initial-password" : "set-password-jit";
await this.router.navigate([route], {
queryParams: {
identifier: orgIdentifier,
},

View File

@@ -27,6 +27,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -75,6 +76,7 @@ describe("TwoFactorAuthComponent", () => {
let mockLoginSuccessHandlerService: MockProxy<LoginSuccessHandlerService>;
let mockTwoFactorAuthCompCacheService: MockProxy<TwoFactorAuthComponentCacheService>;
let mockAuthService: MockProxy<AuthService>;
let mockConfigService: MockProxy<ConfigService>;
let mockUserDecryptionOpts: {
noMasterPassword: UserDecryptionOptions;
@@ -110,6 +112,7 @@ describe("TwoFactorAuthComponent", () => {
mockToastService = mock<ToastService>();
mockTwoFactorAuthCompService = mock<TwoFactorAuthComponentService>();
mockAuthService = mock<AuthService>();
mockConfigService = mock<ConfigService>();
mockEnvService = mock<EnvironmentService>();
mockLoginSuccessHandlerService = mock<LoginSuccessHandlerService>();
@@ -209,6 +212,7 @@ describe("TwoFactorAuthComponent", () => {
useValue: mockTwoFactorAuthCompCacheService,
},
{ provide: AuthService, useValue: mockAuthService },
{ provide: ConfigService, useValue: mockConfigService },
],
});
@@ -225,22 +229,6 @@ describe("TwoFactorAuthComponent", () => {
expect(component).toBeTruthy();
});
// Shared tests
const testChangePasswordOnSuccessfulLogin = () => {
it("navigates to the component's defined change password route when user doesn't have a MP and key connector isn't enabled", async () => {
// Act
await component.submit("testToken");
// Assert
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
expect(mockRouter.navigate).toHaveBeenCalledWith(["set-password"], {
queryParams: {
identifier: component.orgSsoIdentifier,
},
});
});
};
describe("Standard 2FA scenarios", () => {
describe("submit", () => {
const token = "testToken";
@@ -280,20 +268,76 @@ describe("TwoFactorAuthComponent", () => {
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
});
testChangePasswordOnSuccessfulLogin();
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
it("navigates to the /set-initial-password route when user doesn't have a MP and key connector isn't enabled", async () => {
// Arrange
mockConfigService.getFeatureFlag.mockResolvedValue(true);
// Act
await component.submit("testToken");
// Assert
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
expect(mockRouter.navigate).toHaveBeenCalledWith(["set-initial-password"], {
queryParams: {
identifier: component.orgSsoIdentifier,
},
});
});
});
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
it("navigates to the /set-password route when user doesn't have a MP and key connector isn't enabled", async () => {
// Arrange
mockConfigService.getFeatureFlag.mockResolvedValue(false);
// Act
await component.submit("testToken");
// Assert
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
expect(mockRouter.navigate).toHaveBeenCalledWith(["set-password"], {
queryParams: {
identifier: component.orgSsoIdentifier,
},
});
});
});
});
it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => {
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
);
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
it("does not navigate to the /set-initial-password route when the user has key connector even if user has no master password", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await component.submit(token, remember);
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
);
expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-password"], {
queryParams: {
identifier: component.orgSsoIdentifier,
},
await component.submit(token, remember);
expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-initial-password"], {
queryParams: {
identifier: component.orgSsoIdentifier,
},
});
});
});
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
it("does not navigate to the /set-password route when the user has key connector even if user has no master password", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
);
await component.submit(token, remember);
expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-password"], {
queryParams: {
identifier: component.orgSsoIdentifier,
},
});
});
});
});

View File

@@ -32,7 +32,9 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -169,6 +171,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
private loginSuccessHandlerService: LoginSuccessHandlerService,
private twoFactorAuthComponentCacheService: TwoFactorAuthComponentCacheService,
private authService: AuthService,
private configService: ConfigService,
) {}
async ngOnInit() {
@@ -559,7 +562,12 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
}
private async handleChangePasswordRequired(orgIdentifier: string | undefined) {
await this.router.navigate(["set-password"], {
const isSetInitialPasswordRefactorFlagOn = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_SetInitialPasswordRefactor,
);
const route = isSetInitialPasswordRefactorFlagOn ? "set-initial-password" : "set-password";
await this.router.navigate([route], {
queryParams: {
identifier: orgIdentifier,
},

View File

@@ -1,7 +1,16 @@
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
export abstract class AuthRequestApiServiceAbstraction {
/**
* Gets a list of pending auth requests based on the user. There will only be one AuthRequest per device and the
* AuthRequest will be the most recent pending request.
*
* @returns A promise that resolves to a list response containing auth request responses.
*/
abstract getPendingAuthRequests(): Promise<ListResponse<AuthRequestResponse>>;
export abstract class AuthRequestApiService {
/**
* Gets an auth request by its ID.
*

View File

@@ -41,6 +41,12 @@ export abstract class AuthRequestServiceAbstraction {
* @throws If `userId` is not provided.
*/
abstract clearAdminAuthRequest: (userId: UserId) => Promise<void>;
/**
* Gets a list of standard pending auth requests for the user.
* @returns An observable of an array of auth request.
* The array will be empty if there are no pending auth requests.
*/
abstract getPendingAuthRequests$(): Observable<Array<AuthRequestResponse>>;
/**
* Approve or deny an auth request.
* @param approve True to approve, false to deny.

View File

@@ -1,16 +1,23 @@
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { AuthRequestApiService } from "../../abstractions/auth-request-api.service";
import { AuthRequestApiServiceAbstraction } from "../../abstractions/auth-request-api.service";
export class DefaultAuthRequestApiService implements AuthRequestApiService {
export class DefaultAuthRequestApiService implements AuthRequestApiServiceAbstraction {
constructor(
private apiService: ApiService,
private logService: LogService,
) {}
async getPendingAuthRequests(): Promise<ListResponse<AuthRequestResponse>> {
const path = `/auth-requests/pending`;
const r = await this.apiService.send("GET", path, null, true, true);
return new ListResponse(r, AuthRequestResponse);
}
async getAuthRequest(requestId: string): Promise<AuthRequestResponse> {
try {
const path = `/auth-requests/${requestId}`;

View File

@@ -10,23 +10,23 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { StateProvider } from "@bitwarden/common/platform/state";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management";
import { DefaultAuthRequestApiService } from "./auth-request-api.service";
import { AuthRequestService } from "./auth-request.service";
describe("AuthRequestService", () => {
let sut: AuthRequestService;
const stateProvider = mock<StateProvider>();
let accountService: FakeAccountService;
let masterPasswordService: FakeMasterPasswordService;
const appIdService = mock<AppIdService>();
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
const apiService = mock<ApiService>();
const authRequestApiService = mock<DefaultAuthRequestApiService>();
let mockPrivateKey: Uint8Array;
let mockPublicKey: Uint8Array;
@@ -34,17 +34,16 @@ describe("AuthRequestService", () => {
beforeEach(() => {
jest.clearAllMocks();
accountService = mockAccountServiceWith(mockUserId);
masterPasswordService = new FakeMasterPasswordService();
sut = new AuthRequestService(
appIdService,
accountService,
masterPasswordService,
keyService,
encryptService,
apiService,
stateProvider,
authRequestApiService,
);
mockPrivateKey = new Uint8Array(64);

View File

@@ -1,15 +1,15 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable, Subject, firstValueFrom } from "rxjs";
import { Observable, Subject, defer, firstValueFrom, map } from "rxjs";
import { Jsonify } from "type-fest";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -24,6 +24,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management";
import { AuthRequestApiServiceAbstraction } from "../../abstractions/auth-request-api.service";
import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction";
/**
@@ -49,12 +50,12 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
constructor(
private appIdService: AppIdService,
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private keyService: KeyService,
private encryptService: EncryptService,
private apiService: ApiService,
private stateProvider: StateProvider,
private authRequestApiService: AuthRequestApiServiceAbstraction,
) {
this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable();
this.adminLoginApproved$ = this.adminLoginApprovedSubject.asObservable();
@@ -91,6 +92,19 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
await this.stateProvider.setUserState(ADMIN_AUTH_REQUEST_KEY, null, userId);
}
/**
* @description Gets the list of all standard (not admin approval) pending AuthRequests.
*/
getPendingAuthRequests$(): Observable<Array<AuthRequestResponse>> {
return defer(() => this.authRequestApiService.getPendingAuthRequests()).pipe(
map((authRequestResponses: ListResponse<AuthRequestResponse>) => {
return authRequestResponses.data.map((authRequestResponse: AuthRequestResponse) => {
return new AuthRequestResponse(authRequestResponse);
});
}),
);
}
async approveOrDenyAuthRequest(
approve: boolean,
authRequest: AuthRequestResponse,

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

@@ -18,6 +18,7 @@ export class AuthRequestResponse extends BaseResponse {
responseDate?: string;
isAnswered: boolean;
isExpired: boolean;
deviceId?: string; // could be null or empty
constructor(response: any) {
super(response);
@@ -33,6 +34,7 @@ export class AuthRequestResponse extends BaseResponse {
this.creationDate = this.getResponseProperty("CreationDate");
this.requestApproved = this.getResponseProperty("RequestApproved");
this.responseDate = this.getResponseProperty("ResponseDate");
this.deviceId = this.getResponseProperty("RequestDeviceId");
const requestDate = new Date(this.creationDate);
const requestDateUTC = Date.UTC(

View File

@@ -12,13 +12,13 @@ import { ServerConfig } from "../platform/abstractions/config/server-config";
export enum FeatureFlag {
/* Admin Console Team */
SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions",
OptimizeNestedTraverseTypescript = "pm-21695-optimize-nested-traverse-typescript",
CreateDefaultLocation = "pm-19467-create-default-location",
/* Auth */
PM16117_SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor",
PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor",
PM9115_TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence",
PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals",
/* Autofill */
BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain",
@@ -45,7 +45,6 @@ export enum FeatureFlag {
EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation",
/* Tools */
ItemShare = "item-share",
DesktopSendUIRefresh = "desktop-send-ui-refresh",
/* Vault */
@@ -77,7 +76,6 @@ const FALSE = false as boolean;
export const DefaultFeatureFlagValue = {
/* Admin Console Team */
[FeatureFlag.SeparateCustomRolePermissions]: FALSE,
[FeatureFlag.OptimizeNestedTraverseTypescript]: FALSE,
[FeatureFlag.CreateDefaultLocation]: FALSE,
/* Autofill */
@@ -91,7 +89,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.EnableRiskInsightsNotifications]: FALSE,
/* Tools */
[FeatureFlag.ItemShare]: FALSE,
[FeatureFlag.DesktopSendUIRefresh]: FALSE,
/* Vault */
@@ -107,6 +104,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE,
[FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE,
[FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE,
[FeatureFlag.PM14938_BrowserExtensionLoginApproval]: FALSE,
/* Billing */
[FeatureFlag.TrialPaymentOptional]: FALSE,

View File

@@ -14,7 +14,6 @@ import { LogoutReason } from "@bitwarden/auth/common";
import { BiometricsService } from "@bitwarden/key-management";
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
import { SearchService } from "../../../abstractions/search.service";
import { AccountInfo } from "../../../auth/abstractions/account.service";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
@@ -28,6 +27,7 @@ import { StateEventRunnerService } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { CipherService } from "../../../vault/abstractions/cipher.service";
import { FolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
import { SearchService } from "../../../vault/abstractions/search.service";
import { FakeMasterPasswordService } from "../../master-password/services/fake-master-password.service";
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";
import { VaultTimeout, VaultTimeoutStringType } from "../types/vault-timeout.type";

View File

@@ -12,7 +12,6 @@ import { LogoutReason } from "@bitwarden/auth/common";
// eslint-disable-next-line no-restricted-imports
import { BiometricsService } from "@bitwarden/key-management";
import { SearchService } from "../../../abstractions/search.service";
import { AccountService } from "../../../auth/abstractions/account.service";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
@@ -25,6 +24,7 @@ import { StateEventRunnerService } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { CipherService } from "../../../vault/abstractions/cipher.service";
import { FolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
import { SearchService } from "../../../vault/abstractions/search.service";
import { InternalMasterPasswordServiceAbstraction } from "../../master-password/abstractions/master-password.service.abstraction";
import { VaultTimeoutSettingsService } from "../abstractions/vault-timeout-settings.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../abstractions/vault-timeout.service";

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

@@ -4,7 +4,7 @@ import { IpcClient, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-inte
export abstract class IpcService {
private _client?: IpcClient;
protected get client(): IpcClient {
get client(): IpcClient {
if (!this._client) {
throw new Error("IpcService not initialized");
}

View File

@@ -0,0 +1,387 @@
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import {
awaitAsync,
FakeGlobalState,
FakeStateProvider,
mockAccountServiceWith,
} from "../../../../spec";
import { PushTechnology } from "../../../enums/push-technology.enum";
import { UserId } from "../../../types/guid";
import { ConfigService } from "../../abstractions/config/config.service";
import { ServerConfig } from "../../abstractions/config/server-config";
import { Supported } from "../../misc/support-status";
import { Utils } from "../../misc/utils";
import { ServerConfigData } from "../../models/data/server-config.data";
import { PushSettingsConfigResponse } from "../../models/response/server-config.response";
import { KeyDefinition } from "../../state";
import { WebPushNotificationsApiService } from "./web-push-notifications-api.service";
import { WebPushConnector } from "./webpush-connection.service";
import {
WEB_PUSH_SUBSCRIPTION_USERS,
WorkerWebPushConnectionService,
} from "./worker-webpush-connection.service";
const mockUser1 = "testUser1" as UserId;
const createSub = (key: string) => {
return {
options: { applicationServerKey: Utils.fromUrlB64ToArray(key), userVisibleOnly: true },
endpoint: `web.push.endpoint/?${Utils.newGuid()}`,
expirationTime: 5,
getKey: () => null,
toJSON: () => ({ endpoint: "something", keys: {}, expirationTime: 5 }),
unsubscribe: () => Promise.resolve(true),
} satisfies PushSubscription;
};
describe("WorkerWebpushConnectionService", () => {
let configService: MockProxy<ConfigService>;
let webPushApiService: MockProxy<WebPushNotificationsApiService>;
let stateProvider: FakeStateProvider;
let pushManager: MockProxy<PushManager>;
const userId = "testUser1" as UserId;
let sut: WorkerWebPushConnectionService;
beforeEach(() => {
configService = mock();
webPushApiService = mock();
stateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
pushManager = mock();
sut = new WorkerWebPushConnectionService(
configService,
webPushApiService,
mock<ServiceWorkerRegistration>({ pushManager: pushManager }),
stateProvider,
);
});
afterEach(() => {
jest.resetAllMocks();
});
type ExtractKeyDefinitionType<T> = T extends KeyDefinition<infer U> ? U : never;
describe("supportStatus$", () => {
let fakeGlobalState: FakeGlobalState<
ExtractKeyDefinitionType<typeof WEB_PUSH_SUBSCRIPTION_USERS>
>;
beforeEach(() => {
fakeGlobalState = stateProvider.getGlobal(WEB_PUSH_SUBSCRIPTION_USERS) as FakeGlobalState<
ExtractKeyDefinitionType<typeof WEB_PUSH_SUBSCRIPTION_USERS>
>;
});
test("when web push is supported, have an existing subscription, and we've already registered the user, should not call API", async () => {
configService.serverConfig$ = of(
new ServerConfig(
new ServerConfigData({
push: new PushSettingsConfigResponse({
pushTechnology: PushTechnology.WebPush,
vapidPublicKey: "dGVzdA",
}),
}),
),
);
const existingSubscription = createSub("dGVzdA");
await fakeGlobalState.nextState({ [existingSubscription.endpoint]: [userId] });
pushManager.getSubscription.mockResolvedValue(existingSubscription);
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
expect(supportStatus.type).toBe("supported");
const service = (supportStatus as Supported<WebPushConnector>).service;
expect(service).not.toBeFalsy();
const notificationsSub = service.notifications$.subscribe();
await awaitAsync(2);
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(0);
expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(0);
notificationsSub.unsubscribe();
});
test("when web push is supported, have an existing subscription, and we haven't registered the user, should call API", async () => {
configService.serverConfig$ = of(
new ServerConfig(
new ServerConfigData({
push: new PushSettingsConfigResponse({
pushTechnology: PushTechnology.WebPush,
vapidPublicKey: "dGVzdA",
}),
}),
),
);
const existingSubscription = createSub("dGVzdA");
await fakeGlobalState.nextState({
[existingSubscription.endpoint]: ["otherUserId" as UserId],
});
pushManager.getSubscription.mockResolvedValue(existingSubscription);
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
expect(supportStatus.type).toBe("supported");
const service = (supportStatus as Supported<WebPushConnector>).service;
expect(service).not.toBeFalsy();
const notificationsSub = service.notifications$.subscribe();
await awaitAsync(2);
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(1);
expect(fakeGlobalState.nextMock).toHaveBeenCalledWith({
[existingSubscription.endpoint]: ["otherUserId", mockUser1],
});
notificationsSub.unsubscribe();
});
test("when web push is supported, have an existing subscription, but it isn't in state, should call API and add to state", async () => {
configService.serverConfig$ = of(
new ServerConfig(
new ServerConfigData({
push: new PushSettingsConfigResponse({
pushTechnology: PushTechnology.WebPush,
vapidPublicKey: "dGVzdA",
}),
}),
),
);
const existingSubscription = createSub("dGVzdA");
await fakeGlobalState.nextState({
[existingSubscription.endpoint]: null!,
});
pushManager.getSubscription.mockResolvedValue(existingSubscription);
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
expect(supportStatus.type).toBe("supported");
const service = (supportStatus as Supported<WebPushConnector>).service;
expect(service).not.toBeFalsy();
const notificationsSub = service.notifications$.subscribe();
await awaitAsync(2);
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(1);
expect(fakeGlobalState.nextMock).toHaveBeenCalledWith({
[existingSubscription.endpoint]: [mockUser1],
});
notificationsSub.unsubscribe();
});
test("when web push is supported, have an existing subscription, but state array is null, should call API and add to state", async () => {
configService.serverConfig$ = of(
new ServerConfig(
new ServerConfigData({
push: new PushSettingsConfigResponse({
pushTechnology: PushTechnology.WebPush,
vapidPublicKey: "dGVzdA",
}),
}),
),
);
const existingSubscription = createSub("dGVzdA");
await fakeGlobalState.nextState({});
pushManager.getSubscription.mockResolvedValue(existingSubscription);
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
expect(supportStatus.type).toBe("supported");
const service = (supportStatus as Supported<WebPushConnector>).service;
expect(service).not.toBeFalsy();
const notificationsSub = service.notifications$.subscribe();
await awaitAsync(2);
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(1);
expect(fakeGlobalState.nextMock).toHaveBeenCalledWith({
[existingSubscription.endpoint]: [mockUser1],
});
notificationsSub.unsubscribe();
});
test("when web push is supported, but we don't have an existing subscription, should call the api and wipe out existing state", async () => {
configService.serverConfig$ = of(
new ServerConfig(
new ServerConfigData({
push: new PushSettingsConfigResponse({
pushTechnology: PushTechnology.WebPush,
vapidPublicKey: "dGVzdA",
}),
}),
),
);
const existingState = createSub("dGVzdA");
await fakeGlobalState.nextState({ [existingState.endpoint]: [userId] });
pushManager.getSubscription.mockResolvedValue(null);
const newSubscription = createSub("dGVzdA");
pushManager.subscribe.mockResolvedValue(newSubscription);
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
expect(supportStatus.type).toBe("supported");
const service = (supportStatus as Supported<WebPushConnector>).service;
expect(service).not.toBeFalsy();
const notificationsSub = service.notifications$.subscribe();
await awaitAsync(2);
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(1);
expect(fakeGlobalState.nextMock).toHaveBeenCalledWith({
[newSubscription.endpoint]: [mockUser1],
});
notificationsSub.unsubscribe();
});
test("when web push is supported and no existing subscription, should call API", async () => {
configService.serverConfig$ = of(
new ServerConfig(
new ServerConfigData({
push: new PushSettingsConfigResponse({
pushTechnology: PushTechnology.WebPush,
vapidPublicKey: "dGVzdA",
}),
}),
),
);
pushManager.getSubscription.mockResolvedValue(null);
pushManager.subscribe.mockResolvedValue(createSub("dGVzdA"));
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
expect(supportStatus.type).toBe("supported");
const service = (supportStatus as Supported<WebPushConnector>).service;
expect(service).not.toBeFalsy();
const notificationsSub = service.notifications$.subscribe();
await awaitAsync(2);
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
expect(pushManager.subscribe).toHaveBeenCalledTimes(1);
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
notificationsSub.unsubscribe();
});
test("when web push is supported and existing subscription with different key, should call API", async () => {
configService.serverConfig$ = of(
new ServerConfig(
new ServerConfigData({
push: new PushSettingsConfigResponse({
pushTechnology: PushTechnology.WebPush,
vapidPublicKey: "dGVzdA",
}),
}),
),
);
pushManager.getSubscription.mockResolvedValue(createSub("dGVzdF9hbHQ"));
pushManager.subscribe.mockResolvedValue(createSub("dGVzdA"));
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
expect(supportStatus.type).toBe("supported");
const service = (supportStatus as Supported<WebPushConnector>).service;
expect(service).not.toBeFalsy();
const notificationsSub = service.notifications$.subscribe();
await awaitAsync(2);
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
expect(pushManager.subscribe).toHaveBeenCalledTimes(1);
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
notificationsSub.unsubscribe();
});
test("when server config emits multiple times quickly while api call takes a long time will only call API once", async () => {
configService.serverConfig$ = of(
new ServerConfig(
new ServerConfigData({
push: new PushSettingsConfigResponse({
pushTechnology: PushTechnology.WebPush,
vapidPublicKey: "dGVzdA",
}),
}),
),
);
pushManager.getSubscription.mockResolvedValue(createSub("dGVzdF9hbHQ"));
pushManager.subscribe.mockResolvedValue(createSub("dGVzdA"));
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
expect(supportStatus.type).toBe("supported");
const service = (supportStatus as Supported<WebPushConnector>).service;
expect(service).not.toBeFalsy();
const notificationsSub = service.notifications$.subscribe();
await awaitAsync(2);
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
expect(pushManager.subscribe).toHaveBeenCalledTimes(1);
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
notificationsSub.unsubscribe();
});
it("server config shows SignalR support should return not-supported", async () => {
configService.serverConfig$ = of(
new ServerConfig(
new ServerConfigData({
push: new PushSettingsConfigResponse({
pushTechnology: PushTechnology.SignalR,
}),
}),
),
);
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
expect(supportStatus.type).toBe("not-supported");
});
it("server config shows web push but no public key support should return not-supported", async () => {
configService.serverConfig$ = of(
new ServerConfig(
new ServerConfigData({
push: new PushSettingsConfigResponse({
pushTechnology: PushTechnology.WebPush,
vapidPublicKey: null,
}),
}),
),
);
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
expect(supportStatus.type).toBe("not-supported");
});
});
});

View File

@@ -9,6 +9,7 @@ import {
Subject,
Subscription,
switchMap,
withLatestFrom,
} from "rxjs";
import { PushTechnology } from "../../../enums/push-technology.enum";
@@ -17,6 +18,7 @@ import { UserId } from "../../../types/guid";
import { ConfigService } from "../../abstractions/config/config.service";
import { SupportStatus } from "../../misc/support-status";
import { Utils } from "../../misc/utils";
import { KeyDefinition, StateProvider, WEB_PUSH_SUBSCRIPTION } from "../../state";
import { WebPushNotificationsApiService } from "./web-push-notifications-api.service";
import { WebPushConnectionService, WebPushConnector } from "./webpush-connection.service";
@@ -48,6 +50,7 @@ export class WorkerWebPushConnectionService implements WebPushConnectionService
private readonly configService: ConfigService,
private readonly webPushApiService: WebPushNotificationsApiService,
private readonly serviceWorkerRegistration: ServiceWorkerRegistration,
private readonly stateProvider: StateProvider,
) {}
start(): Subscription {
@@ -97,6 +100,7 @@ export class WorkerWebPushConnectionService implements WebPushConnectionService
this.serviceWorkerRegistration,
this.pushEvent,
this.pushChangeEvent,
this.stateProvider,
),
} satisfies SupportStatus<WebPushConnector>;
}),
@@ -114,20 +118,36 @@ class MyWebPushConnector implements WebPushConnector {
private readonly serviceWorkerRegistration: ServiceWorkerRegistration,
private readonly pushEvent$: Observable<PushEvent>,
private readonly pushChangeEvent$: Observable<PushSubscriptionChangeEvent>,
private readonly stateProvider: StateProvider,
) {
const subscriptionUsersState = this.stateProvider.getGlobal(WEB_PUSH_SUBSCRIPTION_USERS);
this.notifications$ = this.getOrCreateSubscription$(this.vapidPublicKey).pipe(
concatMap((subscription) => {
return defer(() => {
if (subscription == null) {
throw new Error("Expected a non-null subscription.");
}
return this.webPushApiService.putSubscription(subscription.toJSON());
}).pipe(
switchMap(() => this.pushEvent$),
map((e) => {
return new NotificationResponse(e.data.json().data);
}),
);
withLatestFrom(subscriptionUsersState.state$.pipe(map((x) => x ?? {}))),
concatMap(async ([[isExistingSubscription, subscription], subscriptionUsers]) => {
if (subscription == null) {
throw new Error("Expected a non-null subscription.");
}
// If this is a new subscription, we can clear state and start over
if (!isExistingSubscription) {
subscriptionUsers = {};
}
// If the user is already subscribed, we don't need to do anything
if (subscriptionUsers[subscription.endpoint]?.includes(this.userId)) {
return;
}
subscriptionUsers[subscription.endpoint] ??= [];
subscriptionUsers[subscription.endpoint].push(this.userId);
// Update the state with the new subscription-user association
await subscriptionUsersState.update(() => subscriptionUsers);
// Inform the server about the new subscription-user association
await this.webPushApiService.putSubscription(subscription.toJSON());
}),
switchMap(() => this.pushEvent$),
map((e) => {
return new NotificationResponse(e.data.json().data);
}),
);
}
@@ -146,7 +166,7 @@ class MyWebPushConnector implements WebPushConnector {
await this.serviceWorkerRegistration.pushManager.getSubscription();
if (existingSubscription == null) {
return await this.pushManagerSubscribe(key);
return [false, await this.pushManagerSubscribe(key)] as const;
}
const subscriptionKey = Utils.fromBufferToUrlB64(
@@ -159,12 +179,30 @@ class MyWebPushConnector implements WebPushConnector {
if (subscriptionKey !== key) {
// There is a subscription, but it's not for the current server, unsubscribe and then make a new one
await existingSubscription.unsubscribe();
return await this.pushManagerSubscribe(key);
return [false, await this.pushManagerSubscribe(key)] as const;
}
return existingSubscription;
return [true, existingSubscription] as const;
}),
this.pushChangeEvent$.pipe(map((event) => event.newSubscription)),
this.pushChangeEvent$.pipe(map((event) => [false, event.newSubscription] as const)),
);
}
}
export const WEB_PUSH_SUBSCRIPTION_USERS = new KeyDefinition<Record<string, UserId[]>>(
WEB_PUSH_SUBSCRIPTION,
"subUsers",
{
deserializer: (obj) => {
if (obj == null) {
return {};
}
const result: Record<string, UserId[]> = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = Array.isArray(value) ? value : [];
}
return result;
},
},
);

View File

@@ -128,6 +128,9 @@ export const EXTENSION_INITIAL_INSTALL_DISK = new StateDefinition(
"extensionInitialInstall",
"disk",
);
export const WEB_PUSH_SUBSCRIPTION = new StateDefinition("webPushSubscription", "disk", {
web: "disk-local",
});
// Design System

View File

@@ -1813,6 +1813,11 @@ export class ApiService implements ApiServiceAbstraction {
if (authed) {
const authHeader = await this.getActiveBearerToken();
headers.set("Authorization", "Bearer " + authHeader);
} else {
// For unauthenticated requests, we need to tell the server what the device is for flag targeting,
// since it won't be able to get it from the access token.
const appId = await this.appIdService.getAppId();
headers.set("Device-Identifier", appId);
}
if (body != null) {

View File

@@ -2,9 +2,9 @@
// @ts-strict-ignore
import { Observable } from "rxjs";
import { SendView } from "../tools/send/models/view/send.view";
import { IndexedEntityId, UserId } from "../types/guid";
import { CipherView } from "../vault/models/view/cipher.view";
import { SendView } from "../../tools/send/models/view/send.view";
import { IndexedEntityId, UserId } from "../../types/guid";
import { CipherView } from "../models/view/cipher.view";
export abstract class SearchService {
indexedEntityId$: (userId: UserId) => Observable<IndexedEntityId | null>;

View File

@@ -0,0 +1,12 @@
import { UnionOfValues } from "../types/union-of-values";
/**
* Available pages within the extension by their URL.
* Useful when opening a specific page within the popup.
*/
export const ExtensionPageUrls: Record<string, `popup/index.html#/${string}`> = {
Index: "popup/index.html#/",
AtRiskPasswords: "popup/index.html#/at-risk-passwords",
} as const;
export type ExtensionPageUrls = UnionOfValues<typeof ExtensionPageUrls>;

View File

@@ -3,3 +3,4 @@ export * from "./cipher-type";
export * from "./field-type.enum";
export * from "./linked-id-type.enum";
export * from "./secure-note-type.enum";
export * from "./extension-page-urls.enum";

View File

@@ -1,7 +1,9 @@
const VaultMessages = {
HasBwInstalled: "hasBwInstalled",
checkBwInstalled: "checkIfBWExtensionInstalled",
/** @deprecated use {@link OpenBrowserExtensionToUrl} */
OpenAtRiskPasswords: "openAtRiskPasswords",
OpenBrowserExtensionToUrl: "openBrowserExtensionToUrl",
PopupOpened: "popupOpened",
} as const;

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

@@ -97,8 +97,8 @@ export class LoginUri extends Domain {
*/
toSdkLoginUri(): SdkLoginUri {
return {
uri: this.uri.toJSON(),
uriChecksum: this.uriChecksum.toJSON(),
uri: this.uri?.toJSON(),
uriChecksum: this.uriChecksum?.toJSON(),
match: this.match,
};
}

View File

@@ -36,24 +36,6 @@ describe("serviceUtils", () => {
});
});
describe("nestedTraverse_vNext", () => {
it("should traverse a tree and add a node at the correct position given a valid path", () => {
const nodeToBeAdded: FakeObject = { id: "1.2.1", name: "1.2.1" };
const path = ["1", "1.2", "1.2.1"];
ServiceUtils.nestedTraverse_vNext(nodeTree, 0, path, nodeToBeAdded, null, "/");
expect(nodeTree[0].children[1].children[0].node).toEqual(nodeToBeAdded);
});
it("should combine the path for missing nodes and use as the added node name given an invalid path", () => {
const nodeToBeAdded: FakeObject = { id: "blank", name: "blank" };
const path = ["3", "3.1", "3.1.1"];
ServiceUtils.nestedTraverse_vNext(nodeTree, 0, path, nodeToBeAdded, null, "/");
expect(nodeTree[2].children[0].node.name).toEqual("3.1/3.1.1");
});
});
describe("getTreeNodeObject", () => {
it("should return a matching node given a single tree branch and a valid id", () => {
const id = "1.1.1";

View File

@@ -4,64 +4,6 @@
import { ITreeNodeObject, TreeNode } from "./models/domain/tree-node";
export class ServiceUtils {
static nestedTraverse(
nodeTree: TreeNode<ITreeNodeObject>[],
partIndex: number,
parts: string[],
obj: ITreeNodeObject,
parent: TreeNode<ITreeNodeObject> | undefined,
delimiter: string,
) {
if (parts.length <= partIndex) {
return;
}
const end: boolean = partIndex === parts.length - 1;
const partName: string = parts[partIndex];
for (let i = 0; i < nodeTree.length; i++) {
if (nodeTree[i].node.name !== partName) {
continue;
}
if (end && nodeTree[i].node.id !== obj.id) {
// Another node exists with the same name as the node being added
nodeTree.push(new TreeNode(obj, parent, partName));
return;
}
// Move down the tree to the next level
ServiceUtils.nestedTraverse(
nodeTree[i].children,
partIndex + 1,
parts,
obj,
nodeTree[i],
delimiter,
);
return;
}
// If there's no node here with the same name...
if (nodeTree.filter((n) => n.node.name === partName).length === 0) {
// And we're at the end of the path given, add the node
if (end) {
nodeTree.push(new TreeNode(obj, parent, partName));
return;
}
// And we're not at the end of the path, combine the current name with the next name
// 1, *1.2, 1.2.1 becomes
// 1, *1.2/1.2.1
const newPartName = partName + delimiter + parts[partIndex + 1];
ServiceUtils.nestedTraverse(
nodeTree,
0,
[newPartName, ...parts.slice(partIndex + 2)],
obj,
parent,
delimiter,
);
}
}
/**
* Recursively adds a node to nodeTree
* @param {TreeNode<ITreeNodeObject>[]} nodeTree - An array of TreeNodes that the node will be added to
@@ -71,7 +13,7 @@ export class ServiceUtils {
* @param {ITreeNodeObject} parent - The parent node of the `obj` node
* @param {string} delimiter - The delimiter used to split the path string, will be used to combine the path for missing nodes
*/
static nestedTraverse_vNext(
static nestedTraverse(
nodeTree: TreeNode<ITreeNodeObject>[],
partIndex: number,
parts: string[],
@@ -104,7 +46,7 @@ export class ServiceUtils {
// 1, *1.2, 1.2.1 becomes
// 1, *1.2/1.2.1
const newPartName = partName + delimiter + parts[partIndex + 1];
ServiceUtils.nestedTraverse_vNext(
ServiceUtils.nestedTraverse(
nodeTree,
0,
[newPartName, ...parts.slice(partIndex + 2)],
@@ -114,7 +56,7 @@ export class ServiceUtils {
);
} else {
// There is a node here with the same name, descend into it
ServiceUtils.nestedTraverse_vNext(
ServiceUtils.nestedTraverse(
matchingNodes[0].children,
partIndex + 1,
parts,

View File

@@ -10,7 +10,6 @@ import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-a
import { FakeStateProvider } from "../../../spec/fake-state-provider";
import { makeStaticByteArray, makeSymmetricCryptoKey } from "../../../spec/utils";
import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service";
import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service";
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
import { BulkEncryptService } from "../../key-management/crypto/abstractions/bulk-encrypt.service";
@@ -29,6 +28,7 @@ import { CipherKey, OrgKey, UserKey } from "../../types/key";
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
import { EncryptionContext } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { SearchService } from "../abstractions/search.service";
import { FieldType } from "../enums";
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
import { CipherType } from "../enums/cipher-type";

View File

@@ -9,7 +9,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { KeyService } from "@bitwarden/key-management";
import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service";
import { AccountService } from "../../auth/abstractions/account.service";
import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service";
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
@@ -38,6 +37,7 @@ import {
EncryptionContext,
} from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { SearchService } from "../abstractions/search.service";
import { FieldType } from "../enums";
import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data";

View File

@@ -4,21 +4,21 @@ import * as lunr from "lunr";
import { Observable, firstValueFrom, map } from "rxjs";
import { Jsonify } from "type-fest";
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
import { UriMatchStrategy } from "../models/domain/domain-service";
import { I18nService } from "../platform/abstractions/i18n.service";
import { LogService } from "../platform/abstractions/log.service";
import { UriMatchStrategy } from "../../models/domain/domain-service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { LogService } from "../../platform/abstractions/log.service";
import {
SingleUserState,
StateProvider,
UserKeyDefinition,
VAULT_SEARCH_MEMORY,
} from "../platform/state";
import { SendView } from "../tools/send/models/view/send.view";
import { IndexedEntityId, UserId } from "../types/guid";
import { FieldType } from "../vault/enums";
import { CipherType } from "../vault/enums/cipher-type";
import { CipherView } from "../vault/models/view/cipher.view";
} from "../../platform/state";
import { SendView } from "../../tools/send/models/view/send.view";
import { IndexedEntityId, UserId } from "../../types/guid";
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
import { FieldType } from "../enums";
import { CipherType } from "../enums/cipher-type";
import { CipherView } from "../models/view/cipher.view";
export type SerializedLunrIndex = {
version: string;

View File

@@ -4,7 +4,7 @@
[icon]="pageIcon"
[showReadonlyHostname]="showReadonlyHostname"
[maxWidth]="maxWidth"
[titleAreaMaxWidth]="titleAreaMaxWidth"
[hideCardWrapper]="hideCardWrapper"
>
<router-outlet></router-outlet>
<router-outlet slot="secondary" name="secondary"></router-outlet>

View File

@@ -10,7 +10,7 @@ import { Translation } from "../dialog";
import { Icon } from "../icon";
import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service";
import { AnonLayoutComponent } from "./anon-layout.component";
import { AnonLayoutComponent, AnonLayoutMaxWidth } from "./anon-layout.component";
export interface AnonLayoutWrapperData {
/**
@@ -36,11 +36,11 @@ export interface AnonLayoutWrapperData {
/**
* Optional flag to set the max-width of the page. Defaults to 'md' if not provided.
*/
maxWidth?: "md" | "3xl";
maxWidth?: AnonLayoutMaxWidth;
/**
* Optional flag to set the max-width of the title area. Defaults to null if not provided.
* Hide the card that wraps the default content. Defaults to false.
*/
titleAreaMaxWidth?: "md";
hideCardWrapper?: boolean;
}
@Component({
@@ -54,8 +54,8 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
protected pageSubtitle: string;
protected pageIcon: Icon;
protected showReadonlyHostname: boolean;
protected maxWidth: "md" | "3xl";
protected titleAreaMaxWidth: "md";
protected maxWidth: AnonLayoutMaxWidth;
protected hideCardWrapper: boolean;
constructor(
private router: Router,
@@ -106,7 +106,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]);
this.maxWidth = firstChildRouteData["maxWidth"];
this.titleAreaMaxWidth = firstChildRouteData["titleAreaMaxWidth"];
this.hideCardWrapper = Boolean(firstChildRouteData["hideCardWrapper"]);
}
private listenForServiceDataChanges() {
@@ -143,6 +143,10 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
this.showReadonlyHostname = data.showReadonlyHostname;
}
if (data.hideCardWrapper !== undefined) {
this.hideCardWrapper = data.hideCardWrapper;
}
// Manually fire change detection to avoid ExpressionChangedAfterItHasBeenCheckedError
// when setting the page data from a service
this.changeDetectorRef.detectChanges();
@@ -164,7 +168,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
this.pageIcon = null;
this.showReadonlyHostname = null;
this.maxWidth = null;
this.titleAreaMaxWidth = null;
this.hideCardWrapper = null;
}
ngOnDestroy() {

View File

@@ -87,6 +87,7 @@ const decorators = (options: {
appLogoLabel: "app logo label",
finishCreatingYourAccountBySettingAPassword:
"Finish creating your account by setting a password",
enterpriseSingleSignOn: "Enterprise Single Sign-On",
});
},
},

View File

@@ -13,11 +13,8 @@
<bit-icon [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n"></bit-icon>
</a>
<div
class="tw-text-center tw-mb-4 sm:tw-mb-6"
[ngClass]="{ 'tw-max-w-md tw-mx-auto': titleAreaMaxWidth === 'md' }"
>
<div *ngIf="!hideIcon" class="tw-mx-auto tw-max-w-24 sm:tw-max-w-28 md:tw-max-w-32">
<div class="tw-text-center tw-mb-4 sm:tw-mb-6 tw-mx-auto" [ngClass]="maxWidthClass">
<div *ngIf="!hideIcon" class="tw-w-24 sm:tw-w-28 md:tw-w-32 tw-mx-auto">
<bit-icon [icon]="icon"></bit-icon>
</div>
@@ -36,14 +33,20 @@
</div>
<div
class="tw-grow tw-w-full tw-max-w-md tw-mx-auto tw-flex tw-flex-col tw-items-center sm:tw-min-w-[28rem]"
[ngClass]="{ 'tw-max-w-md': maxWidth === 'md', 'tw-max-w-3xl': maxWidth === '3xl' }"
class="tw-grow tw-w-full tw-mx-auto tw-flex tw-flex-col tw-items-center sm:tw-min-w-[28rem]"
[ngClass]="maxWidthClass"
>
<div
class="tw-rounded-2xl tw-mb-6 sm:tw-mb-10 tw-mx-auto tw-w-full sm:tw-bg-background sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8"
>
<ng-content></ng-content>
</div>
@if (hideCardWrapper) {
<div class="tw-mb-6 sm:tw-mb-10">
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
</div>
} @else {
<div
class="tw-rounded-2xl tw-mb-6 sm:tw-mb-10 tw-mx-auto tw-w-full sm:tw-bg-background sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8"
>
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
</div>
}
<ng-content select="[slot=secondary]"></ng-content>
</div>
@@ -60,3 +63,7 @@
</ng-container>
</footer>
</main>
<ng-template #defaultContent>
<ng-content></ng-content>
</ng-template>

View File

@@ -14,6 +14,8 @@ import { BitwardenLogo, BitwardenShield } from "../icon/icons";
import { SharedModule } from "../shared";
import { TypographyModule } from "../typography";
export type AnonLayoutMaxWidth = "md" | "lg" | "xl" | "2xl" | "3xl";
@Component({
selector: "auth-anon-layout",
templateUrl: "./anon-layout.component.html",
@@ -33,29 +35,38 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
@Input() hideLogo: boolean = false;
@Input() hideFooter: boolean = false;
@Input() hideIcon: boolean = false;
@Input() hideCardWrapper: boolean = false;
/**
* Max width of the title area content
*
* @default null
*/
@Input() titleAreaMaxWidth?: "md";
/**
* Max width of the layout content
* Max width of the anon layout title, subtitle, and content areas.
*
* @default 'md'
*/
@Input() maxWidth: "md" | "3xl" = "md";
@Input() maxWidth: AnonLayoutMaxWidth = "md";
protected logo = BitwardenLogo;
protected year = "2024";
protected year: string;
protected clientType: ClientType;
protected hostname: string;
protected version: string;
protected hideYearAndVersion = false;
get maxWidthClass(): string {
switch (this.maxWidth) {
case "md":
return "tw-max-w-md";
case "lg":
return "tw-max-w-lg";
case "xl":
return "tw-max-w-xl";
case "2xl":
return "tw-max-w-2xl";
case "3xl":
return "tw-max-w-3xl";
}
}
constructor(
private environmentService: EnvironmentService,
private platformUtilsService: PlatformUtilsService,
@@ -67,7 +78,6 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
async ngOnInit() {
this.maxWidth = this.maxWidth ?? "md";
this.titleAreaMaxWidth = this.titleAreaMaxWidth ?? null;
this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname();
this.version = await this.platformUtilsService.getApplicationVersion();

View File

@@ -165,4 +165,4 @@ import { EnvironmentSelectorComponent } from "./components/environment-selector/
---
<Story of={stories.WithSecondaryContent} />
<Story of={stories.SecondaryContent} />

View File

@@ -8,6 +8,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ButtonModule } from "../button";
import { Icon } from "../icon";
import { LockIcon } from "../icon/icons";
import { I18nMockService } from "../utils/i18n-mock.service";
@@ -18,6 +19,23 @@ class MockPlatformUtilsService implements Partial<PlatformUtilsService> {
getClientType = () => ClientType.Web;
}
type StoryArgs = Pick<
AnonLayoutComponent,
| "title"
| "subtitle"
| "showReadonlyHostname"
| "hideCardWrapper"
| "hideIcon"
| "hideLogo"
| "hideFooter"
| "maxWidth"
> & {
contentLength: "normal" | "long" | "thin";
showSecondary: boolean;
useDefaultIcon: boolean;
icon: Icon;
};
export default {
title: "Component Library/Anon Layout",
component: AnonLayoutComponent,
@@ -31,12 +49,11 @@ export default {
},
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
useFactory: () =>
new I18nMockService({
accessing: "Accessing",
appLogoLabel: "app logo label",
});
},
}),
},
{
provide: EnvironmentService,
@@ -55,174 +72,179 @@ export default {
],
}),
],
render: (args: StoryArgs) => {
const { useDefaultIcon, icon, ...rest } = args;
return {
props: {
...rest,
icon: useDefaultIcon ? null : icon,
},
template: `
<auth-anon-layout
[title]="title"
[subtitle]="subtitle"
[icon]="icon"
[showReadonlyHostname]="showReadonlyHostname"
[maxWidth]="maxWidth"
[hideCardWrapper]="hideCardWrapper"
[hideIcon]="hideIcon"
[hideLogo]="hideLogo"
[hideFooter]="hideFooter"
>
<ng-container [ngSwitch]="contentLength">
<div *ngSwitchCase="'thin'" class="tw-text-center"> <div class="tw-font-bold">Thin Content</div></div>
<div *ngSwitchCase="'long'">
<div class="tw-font-bold">Long Content</div>
<div>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?</div>
<div>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?</div>
</div>
<div *ngSwitchDefault>
<div class="tw-font-bold">Normal Content</div>
<div>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. </div>
</div>
</ng-container>
<div *ngIf="showSecondary" slot="secondary" class="tw-text-center">
<div class="tw-font-bold tw-mb-2">
Secondary Projected Content (optional)
</div>
<button bitButton>Perform Action</button>
</div>
</auth-anon-layout>
`,
};
},
argTypes: {
title: { control: "text" },
subtitle: { control: "text" },
icon: { control: false, table: { disable: true } },
useDefaultIcon: {
control: false,
table: { disable: true },
description: "If true, passes null so component falls back to its built-in icon",
},
showReadonlyHostname: { control: "boolean" },
maxWidth: {
control: "select",
options: ["md", "lg", "xl", "2xl", "3xl"],
},
hideCardWrapper: { control: "boolean" },
hideIcon: { control: "boolean" },
hideLogo: { control: "boolean" },
hideFooter: { control: "boolean" },
contentLength: {
control: "radio",
options: ["normal", "long", "thin"],
},
showSecondary: { control: "boolean" },
},
args: {
title: "The Page Title",
subtitle: "The subtitle (optional)",
showReadonlyHostname: true,
icon: LockIcon,
useDefaultIcon: false,
showReadonlyHostname: false,
maxWidth: "md",
hideCardWrapper: false,
hideIcon: false,
hideLogo: false,
hideFooter: false,
contentLength: "normal",
showSecondary: false,
},
} as Meta;
} as Meta<StoryArgs>;
type Story = StoryObj<AnonLayoutComponent>;
type Story = StoryObj<StoryArgs>;
export const WithPrimaryContent: Story = {
render: (args) => ({
props: args,
template:
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
`
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="hideLogo" >
<div>
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
</div>
</auth-anon-layout>
`,
}),
export const NormalPrimaryContent: Story = {
args: {
contentLength: "normal",
},
};
export const WithSecondaryContent: Story = {
render: (args) => ({
props: args,
template:
// Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling.
// Notice that slot="secondary" is requred to project any secondary content.
`
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="hideLogo" >
<div>
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
</div>
<div slot="secondary" class="text-center">
<div class="tw-font-bold tw-mb-2">Secondary Projected Content (optional)</div>
<button bitButton>Perform Action</button>
</div>
</auth-anon-layout>
`,
}),
export const LongPrimaryContent: Story = {
args: {
contentLength: "long",
},
};
export const WithLongContent: Story = {
render: (args) => ({
props: args,
template:
// Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling.
`
<auth-anon-layout title="Page Title lorem ipsum dolor consectetur sit amet expedita quod est" subtitle="Subtitle here Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, quod est?" [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="hideLogo" >
<div>
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.</div>
</div>
<div slot="secondary" class="text-center">
<div class="tw-font-bold tw-mb-2">Secondary Projected Content (optional)</div>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Expedita, quod est? </p>
<button bitButton>Perform Action</button>
</div>
</auth-anon-layout>
`,
}),
export const ThinPrimaryContent: Story = {
args: {
contentLength: "thin",
},
};
export const WithThinPrimaryContent: Story = {
render: (args) => ({
props: args,
template:
// Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling.
`
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="hideLogo" >
<div class="text-center">Lorem ipsum</div>
<div slot="secondary" class="text-center">
<div class="tw-font-bold tw-mb-2">Secondary Projected Content (optional)</div>
<button bitButton>Perform Action</button>
</div>
</auth-anon-layout>
`,
}),
export const LongContentAndTitlesAndDefaultWidth: Story = {
args: {
title:
"This is a very long title that might not fit in the default width. It's really long and descriptive, so it might take up more space than usual.",
subtitle:
"This is a very long subtitle that might not fit in the default width. It's really long and descriptive, so it might take up more space than usual.",
contentLength: "long",
},
};
export const WithCustomIcon: Story = {
render: (args) => ({
props: args,
template:
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
`
<auth-anon-layout [title]="title" [subtitle]="subtitle" [icon]="icon" [showReadonlyHostname]="showReadonlyHostname">
<div>
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
</div>
</auth-anon-layout>
`,
}),
export const LongContentAndTitlesAndLargestWidth: Story = {
args: {
title:
"This is a very long title that might not fit in the default width. It's really long and descriptive, so it might take up more space than usual.",
subtitle:
"This is a very long subtitle that might not fit in the default width. It's really long and descriptive, so it might take up more space than usual.",
contentLength: "long",
maxWidth: "3xl",
},
};
export const HideIcon: Story = {
render: (args) => ({
props: args,
template:
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
`
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideIcon]="true" >
<div>
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
</div>
</auth-anon-layout>
`,
}),
export const SecondaryContent: Story = {
args: {
showSecondary: true,
},
};
export const HideLogo: Story = {
render: (args) => ({
props: args,
template:
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
`
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="true" >
<div>
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
</div>
</auth-anon-layout>
`,
}),
export const NoTitle: Story = { args: { title: undefined } };
export const NoSubtitle: Story = { args: { subtitle: undefined } };
export const NoWrapper: Story = {
args: { hideCardWrapper: true },
};
export const HideFooter: Story = {
render: (args) => ({
props: args,
template:
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
`
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideFooter]="true" [hideLogo]="hideLogo" >
<div>
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
</div>
</auth-anon-layout>
`,
}),
export const DefaultIcon: Story = {
args: { useDefaultIcon: true },
};
export const WithTitleAreaMaxWidth: Story = {
render: (args) => ({
props: {
...args,
title: "This is a very long long title to demonstrate titleAreaMaxWidth set to 'md'",
subtitle:
"This is a very long subtitle that demonstrates how the max width container handles longer text content with the titleAreaMaxWidth input set to 'md'. Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, quod est?",
},
template: `
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="hideLogo" [titleAreaMaxWidth]="'md'">
<div>
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
</div>
</auth-anon-layout>
`,
}),
export const NoIcon: Story = {
args: { hideIcon: true },
};
export const NoLogo: Story = {
args: { hideLogo: true },
};
export const NoFooter: Story = {
args: { hideFooter: true },
};
export const ReadonlyHostname: Story = {
args: { showReadonlyHostname: true },
};
export const MinimalState: Story = {
args: {
title: undefined,
subtitle: undefined,
contentLength: "normal",
hideCardWrapper: true,
hideIcon: true,
hideLogo: true,
hideFooter: true,
},
};

View File

@@ -41,12 +41,14 @@ import { IconModule } from "@bitwarden/components";
- A non-comprehensive list of common colors and their associated classes is below:
| Hardcoded Value | Tailwind Stroke Class | Tailwind Fill Class | Tailwind Variable |
| ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ----------------------- | ----------------------- |
| `#020F66` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#020F66"}}></span> | `tw-stroke-art-primary` | `tw-fill-art-primary` | `--color-art-primary` |
| `#10949D` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#10949D"}}></span> | `tw-stroke-art-accent` | `tw-fill-art-accent` | `--color-art-accent` |
| `#2CDDE9` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#2CDDE9"}}></span> | `tw-stroke-art-accent` | `tw-fill-art-accent` | `--color-art-accent` |
| `#89929F` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#89929F"}}></span> | `tw-stroke-secondary-600` | `tw-fill-secondary-600` | `--color-secondary-600` |
| Hardcoded Value | Tailwind Stroke Class | Tailwind Fill Class | Tailwind Variable |
| ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ----------------------------------- | ----------------------------------- |
| `#020F66` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#020F66"}}></span> | `tw-stroke-illustration-outline` | `tw-fill-illustration-outline` | `--color-illustration-outline` |
| `#DBE5F6` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#DBE5F6"}}></span> | `tw-stroke-illustration-bg-primary` | `tw-fill-illustration-bg-primary` | `--color-illustration-bg-primary` |
| `#AAC3EF` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#AAC3EF"}}></span> | `tw-stroke-illustration-bg-secondary` | `tw-fill-illustration-bg-secondary` | `--color-illustration-bg-secondary` |
| `#FFFFFF` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#FFFFFF"}}></span> | `tw-stroke-illustration-bg-tertiary` | `tw-fill-illustration-bg-tertiary` | `--color-illustration-bg-tertiary` |
| `#FFBF00` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#FFBF00"}}></span> | `tw-stroke-illustration-tertiary` | `tw-fill-illustration-tertiary` | `--color-illustration-tertiary` |
| `#175DDC` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#175DDC"}}></span> | `tw-stroke-illustration-logo` | `tw-fill-illustration-logo` | `--color-illustration-logo` |
- If the hex that you have on an SVG path is not listed above, there are a few ways to figure out
the appropriate Tailwind class:
@@ -56,20 +58,20 @@ import { IconModule } from "@bitwarden/components";
- Click on an individual path on the SVG until you see the path's properties in the
right-hand panel.
- Scroll down to the Colors section.
- Example: `Color/Art/Primary`
- Example: `Color/Illustration/Outline`
- This also includes Hex or RGB values that can be used to find the appropriate Tailwind
variable as well if you follow the manual search option below.
- Create the appropriate stroke or fill class from the color used.
- Example: `Color/Art/Primary` corresponds to `--color-art-primary` which corresponds to
`tw-stroke-art-primary` or `tw-fill-art-primary`.
- Example: `Color/Illustration/Outline` corresponds to `--color-illustration-outline` which
corresponds to `tw-stroke-illustration-outline` or `tw-fill-illustration-outline`.
- **Option 2: Manual Search**
- Take the path's stroke or fill hex value and convert it to RGB using a tool like
[Hex to RGB](https://www.rgbtohex.net/hex-to-rgb/).
- Search for the RGB value without commas in our `tw-theme.css` to find the Tailwind variable
that corresponds to the color.
- Create the appropriate stroke or fill class using the Tailwind variable.
- Example: `--color-art-primary` corresponds to `tw-stroke-art-primary` or
`tw-fill-art-primary`.
- Example: `--color-illustration-outline` corresponds to `tw-stroke-illustration-outline`
or `tw-fill-illustration-outline`.
6. **Remove any hardcoded width or height attributes** if your SVG has a configured
[viewBox](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox) attribute in order

View File

@@ -1,5 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { hasModifierKey } from "@angular/cdk/keycodes";
import { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay";
import { TemplatePortal } from "@angular/cdk/portal";
import {
@@ -76,6 +77,13 @@ export class MenuTriggerForDirective implements OnDestroy {
this.overlayRef.attach(templatePortal);
this.closedEventsSub = this.getClosedEvents().subscribe((event: KeyboardEvent | undefined) => {
// Closing the menu is handled in this.destroyMenu, so we want to prevent the escape key
// from doing its normal default action, which would otherwise cause a parent component
// (like a dialog) or extension window to close
if (event?.key === "Escape" && !hasModifierKey(event)) {
event.preventDefault();
}
if (["Tab", "Escape"].includes(event?.key)) {
// Required to ensure tab order resumes correctly
this.elementRef.nativeElement.focus();

View File

@@ -9,6 +9,7 @@
[clearable]="false"
(close)="onClose()"
appendTo="body"
[keyDownFn]="onKeyDown"
>
<ng-template ng-option-tmp let-item="item">
<div class="tw-flex" [title]="item.label">

View File

@@ -1,6 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { hasModifierKey } from "@angular/cdk/keycodes";
import {
Component,
ContentChildren,
@@ -185,4 +186,20 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
protected onClose() {
this.closed.emit();
}
/**
* Prevent Escape key press from propagating to parent components
* (for example, parent dialog should not close when Escape is pressed in the select)
*
* @returns true to keep default key behavior; false to prevent default key behavior
*
* Needs to be arrow function to retain `this` scope.
*/
protected onKeyDown = (event: KeyboardEvent) => {
if (this.select.isOpen && event.key === "Escape" && !hasModifierKey(event)) {
event.stopPropagation();
}
return true;
};
}

View File

@@ -62,9 +62,14 @@ export const Table = (args) => (
{Row("notification-600")}
</tbody>
<tbody>
{Row("art-primary")}
{Row("art-accent")}
{Row("illustration-outline")}
{Row("illustration-bg-primary")}
{Row("illustration-bg-secondary")}
{Row("illustration-bg-tertiary")}
{Row("illustration-tertiary")}
{Row("illustration-logo")}
</tbody>
<thead>
<tr>
<th>Text</th>
@@ -78,6 +83,7 @@ export const Table = (args) => (
{Row("text-alt2")}
{Row("text-code")}
</tbody>
</table>
);

View File

@@ -16,7 +16,7 @@ We export a similar directive, `bitScrollLayout`, that integrates with `bit-layo
and should be used instead of `scrollWindow`.
```html
<!-- Descendant of bit-layout -->
<!-- Descendant of bit-layout or popup-page -->
<cdk-virtual-scroll-viewport bitScrollLayout>
<!-- virtual scroll implementation here -->
</cdk-virtual-scroll-viewport>
@@ -27,7 +27,10 @@ and should be used instead of `scrollWindow`.
Due to the initialization order of Angular components and their templates, `bitScrollLayout` will
error if it is used _in the same template_ as the layout component:
With `bit-layout`:
```html
<!-- Will cause `bitScrollLayout` to error -->
<bit-layout>
<cdk-virtual-scroll-viewport bitScrollLayout>
<!-- virtual scroll implementation here -->
@@ -35,20 +38,43 @@ error if it is used _in the same template_ as the layout component:
</bit-layout>
```
With `popup-page`:
```html
<!-- Will cause `bitScrollLayout` to error -->
<popup-page>
<cdk-virtual-scroll-viewport bitScrollLayout>
<!-- virtual scroll implementation here -->
</cdk-virtual-scroll-viewport>
</popup-page>
```
In this particular composition, the child content gets constructed before the template of
`bit-layout` and thus has no scroll container to reference. Workarounds include:
`bit-layout` (or `popup-page`) and thus has no scroll container to reference. Workarounds include:
1. Wrap the child in another component. (This tends to happen by default when the layout is
integrated with a `router-outlet`.)
With `bit-layout`:
```html
<bit-layout>
<component-that-contains-bitScrollLayout></component-that-contains-bitScrollLayout>
</bit-layout>
```
With `popup-page`:
```html
<popup-page>
<component-that-contains-bitScrollLayout></component-that-contains-bitScrollLayout>
</popup-page>
```
2. Use a `defer` block.
With `bit-layout`:
```html
<bit-layout>
@defer (on immediate) {
@@ -58,3 +84,15 @@ In this particular composition, the child content gets constructed before the te
}
</bit-layout>
```
With `popup-page`:
```html
<popup-page>
@defer (on immediate) {
<cdk-virtual-scroll-viewport bitScrollLayout>
<!-- virtual scroll implementation here -->
</div>
}
</popup-page>
```

View File

@@ -46,6 +46,7 @@
--color-notification-100: 255 225 247;
--color-notification-600: 192 17 118;
/*art styles deprecated, use 'illustration' instead*/
--color-art-primary: 2 15 102;
--color-art-accent: 44 221 223;
@@ -58,6 +59,15 @@
--color-marketing-logo: 23 93 220;
--tw-ring-offset-color: #ffffff;
--tw-sm-breakpoint: 640px;
--color-illustration-outline: 2 15 102;
--color-illustration-bg-primary: 219 229 246;
--color-illustration-bg-secondary: 170 195 239;
--color-illustration-bg-tertiary: 255 255 255;
--color-illustration-tertiary: 255 191 0;
--color-illustration-logo: 23 93 220;
}
.theme_light {
@@ -104,6 +114,7 @@
--color-notification-100: 117 37 83;
--color-notification-600: 255 143 208;
/*art styles deprecated, use 'illustration' instead*/
--color-art-primary: 243 246 249;
--color-art-accent: 44 221 233;
@@ -116,6 +127,13 @@
--color-marketing-logo: 255 255 255;
--tw-ring-offset-color: #1f242e;
--color-illustration-outline: 23 93 220;
--color-illustration-bg-primary: 170 195 239;
--color-illustration-bg-secondary: 121 161 233;
--color-illustration-bg-tertiary: 243 246 249;
--color-illustration-tertiary: 255 191 0;
--color-illustration-logo: 255 255 255;
}
/**

View File

@@ -63,6 +63,7 @@ module.exports = {
100: rgba("--color-notification-100"),
600: rgba("--color-notification-600"),
},
// art styles deprecated, use 'illustration' instead
art: {
primary: rgba("--color-art-primary"),
accent: rgba("--color-art-accent"),
@@ -83,6 +84,14 @@ module.exports = {
alt4: rgba("--color-background-alt4"),
},
"marketing-logo": rgba("--color-marketing-logo"),
illustration: {
outline: rgba("--color-illustration-outline"),
"bg-primary": rgba("--color-illustration-bg-primary"),
"bg-secondary": rgba("--color-illustration-bg-secondary"),
"bg-tertiary": rgba("--color-illustration-bg-tertiary"),
tertiary: rgba("--color-illustration-tertiary"),
logo: rgba("--color-illustration-logo"),
},
},
textColor: {
main: rgba("--color-text-main"),

View File

@@ -435,6 +435,12 @@
and select Export items &rarr; Enter your Master Password and select Continue. &rarr; Save
the CSV file on your device.
</ng-container>
<ng-container *ngIf="format === 'passworddepot17xml'">
On the desktop application, go to Tools &rarr; Export &rarr; Enter your master password
&rarr; Select XML Format (*.xml) as Export format &rarr; Click on next &rarr; Choose which
entries should be included in the export &rarr; Click on next to export into the location
previously chosen.
</ng-container>
</bit-callout>
<import-lastpass
*ngIf="showLastPassOptions"

View File

@@ -42,6 +42,7 @@ export { PassmanJsonImporter } from "./passman-json-importer";
export { PasspackCsvImporter } from "./passpack-csv-importer";
export { PasswordAgentCsvImporter } from "./passwordagent-csv-importer";
export { PasswordBossJsonImporter } from "./passwordboss-json-importer";
export { PasswordDepot17XmlImporter } from "./password-depot";
export { PasswordDragonXmlImporter } from "./passworddragon-xml-importer";
export { PasswordSafeXmlImporter } from "./passwordsafe-xml-importer";
export { PasswordWalletTxtImporter } from "./passwordwallet-txt-importer";

View File

@@ -0,0 +1 @@
export { PasswordDepot17XmlImporter } from "./password-depot-17-xml-importer";

View File

@@ -0,0 +1,62 @@
import {
MacOS_MultipleFolders,
MacOS_PasswordDepotXmlFile,
MacOS_WrongVersion,
} from "../spec-data/password-depot-xml";
import { PasswordDepot17XmlImporter } from "./password-depot-17-xml-importer";
describe("Password Depot 17 MacOS Xml Importer", () => {
it("should return error with invalid export version", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(MacOS_WrongVersion);
expect(result.errorMessage).toBe(
"Unsupported export version detected - (only 17.0 is supported)",
);
});
it("should not create a folder/collection if the group fingerprint is null", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(MacOS_PasswordDepotXmlFile);
expect(result.folders.length).toBe(0);
});
it("should create folders and with correct assignments", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(MacOS_MultipleFolders);
// Expect 10 ciphers, 5 without a folder and 3 within 'folder macos' and 2 with 'folder 2'
expect(result.ciphers.length).toBe(10);
expect(result.folders.length).toBe(2);
expect(result.folders[0].name).toBe("folder macos");
expect(result.folders[1].name).toBe("folder 2");
// 3 items within 'folder macos'
expect(result.folderRelationships[0]).toEqual([5, 0]);
expect(result.folderRelationships[1]).toEqual([6, 0]);
expect(result.folderRelationships[2]).toEqual([7, 0]);
//2 items with 'folder 2'
expect(result.folderRelationships[3]).toEqual([8, 1]);
expect(result.folderRelationships[4]).toEqual([9, 1]);
});
it("should parse custom fields from a MacOS exported file", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(MacOS_PasswordDepotXmlFile);
const cipher = result.ciphers.shift();
expect(cipher.name).toBe("card 1");
expect(cipher.notes).toBe("comment");
expect(cipher.card).not.toBeNull();
expect(cipher.card.cardholderName).toBe("some CC holder");
expect(cipher.card.number).toBe("4242424242424242");
expect(cipher.card.brand).toBe("Visa");
expect(cipher.card.expMonth).toBe("8");
expect(cipher.card.expYear).toBe("2028");
expect(cipher.card.code).toBe("125");
});
});

View File

@@ -0,0 +1,496 @@
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { CollectionView } from "@bitwarden/admin-console/common";
import { FieldType, SecureNoteType } from "@bitwarden/common/vault/enums";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { CipherType } from "@bitwarden/sdk-internal";
import {
EncryptedFileData,
InvalidRootNodeData,
InvalidVersionData,
CreditCardTestData,
MissingPasswordsNodeData,
PasswordTestData,
IdentityTestData,
RDPTestData,
SoftwareLicenseTestData,
TeamViewerTestData,
PuttyTestData,
BankingTestData,
InformationTestData,
CertificateTestData,
EncryptedFileTestData,
DocumentTestData,
} from "../spec-data/password-depot-xml";
import { PasswordDepot17XmlImporter } from "./password-depot-17-xml-importer";
describe("Password Depot 17 Xml Importer", () => {
it("should return error with missing root tag", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(InvalidRootNodeData);
expect(result.errorMessage).toBe("Missing `passwordfile` node.");
});
it("should return error with invalid export version", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(InvalidVersionData);
expect(result.errorMessage).toBe(
"Unsupported export version detected - (only 17.0 is supported)",
);
});
it("should return error if file is marked as encrypted", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(EncryptedFileData);
expect(result.errorMessage).toBe("Encrypted Password Depot files are not supported.");
});
it("should return error with missing passwords node tag", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(MissingPasswordsNodeData);
expect(result.success).toBe(false);
expect(result.errorMessage).toBe("Missing `passwordfile > passwords` node.");
});
it("should parse groups nodes into folders", async () => {
const importer = new PasswordDepot17XmlImporter();
const folder = new FolderView();
folder.name = "tempDB";
const actual = [folder];
const result = await importer.parse(PasswordTestData);
expect(result.folders).toEqual(actual);
});
it("should parse password type into logins", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(PasswordTestData);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.Login);
expect(cipher.name).toBe("password type");
expect(cipher.notes).toBe("someComment");
expect(cipher.login).not.toBeNull();
expect(cipher.login.username).toBe("someUser");
expect(cipher.login.password).toBe("p6J<]fmjv!:H&iJ7/Mwt@3i8");
expect(cipher.login.uri).toBe("http://example.com");
});
it("should parse any unmapped fields as custom fields", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(PasswordTestData);
const cipher = result.ciphers.shift();
expect(cipher.type).toBe(CipherType.Login);
expect(cipher.name).toBe("password type");
expect(cipher.fields).not.toBeNull();
expect(cipher.fields[0].name).toBe("lastmodified");
expect(cipher.fields[0].value).toBe("07.05.2025 13:37:56");
expect(cipher.fields[0].type).toBe(FieldType.Text);
expect(cipher.fields[1].name).toBe("expirydate");
expect(cipher.fields[1].value).toBe("07.05.2025");
expect(cipher.fields[0].type).toBe(FieldType.Text);
expect(cipher.fields[2].name).toBe("importance");
expect(cipher.fields[2].value).toBe("0");
let customField = cipher.fields.find((f) => f.name === "passwort");
expect(customField).toBeDefined();
expect(customField.value).toEqual("password");
expect(customField.type).toEqual(FieldType.Hidden);
customField = cipher.fields.find((f) => f.name === "memo");
expect(customField).toBeDefined();
expect(customField.value).toEqual("memo");
expect(customField.type).toEqual(FieldType.Text);
customField = cipher.fields.find((f) => f.name === "datum");
expect(customField).toBeDefined();
const expectedDate = new Date("2025-05-13T00:00:00Z");
expect(customField.value).toEqual(expectedDate.toLocaleDateString());
expect(customField.type).toEqual(FieldType.Text);
customField = cipher.fields.find((f) => f.name === "nummer");
expect(customField).toBeDefined();
expect(customField.value).toEqual("1");
expect(customField.type).toEqual(FieldType.Text);
customField = cipher.fields.find((f) => f.name === "boolean");
expect(customField).toBeDefined();
expect(customField.value).toEqual("1");
expect(customField.type).toEqual(FieldType.Boolean);
customField = cipher.fields.find((f) => f.name === "decimal");
expect(customField).toBeDefined();
expect(customField.value).toEqual("1,01");
expect(customField.type).toEqual(FieldType.Text);
customField = cipher.fields.find((f) => f.name === "email");
expect(customField).toBeDefined();
expect(customField.value).toEqual("who@cares.com");
expect(customField.type).toEqual(FieldType.Text);
customField = cipher.fields.find((f) => f.name === "url");
expect(customField).toBeDefined();
expect(customField.value).toEqual("example.com");
expect(customField.type).toEqual(FieldType.Text);
});
it("should parse credit cards", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(CreditCardTestData);
const cipher = result.ciphers.shift();
expect(cipher.name).toBe("some CreditCard");
expect(cipher.notes).toBe("someComment");
expect(cipher.card).not.toBeNull();
expect(cipher.card.cardholderName).toBe("some CC holder");
expect(cipher.card.number).toBe("4222422242224222");
expect(cipher.card.brand).toBe("Visa");
expect(cipher.card.expMonth).toBe("5");
expect(cipher.card.expYear).toBe("2026");
expect(cipher.card.code).toBe("123");
});
it("should parse identity type", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(IdentityTestData);
const cipher = result.ciphers.shift();
expect(cipher.name).toBe("identity type");
expect(cipher.notes).toBe("someNote");
expect(cipher.identity).not.toBeNull();
expect(cipher.identity.firstName).toBe("firstName");
expect(cipher.identity.lastName).toBe("surName");
expect(cipher.identity.email).toBe("email");
expect(cipher.identity.company).toBe("someCompany");
expect(cipher.identity.address1).toBe("someStreet");
expect(cipher.identity.address2).toBe("address 2");
expect(cipher.identity.city).toBe("town");
expect(cipher.identity.state).toBe("state");
expect(cipher.identity.postalCode).toBe("zipCode");
expect(cipher.identity.country).toBe("country");
expect(cipher.identity.phone).toBe("phoneNumber");
});
it("should parse RDP type", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(RDPTestData);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.Login);
expect(cipher.name).toBe("rdp type");
expect(cipher.notes).toBe("someNote");
expect(cipher.login).not.toBeNull();
expect(cipher.login.username).toBe("someUser");
expect(cipher.login.password).toBe("somePassword");
expect(cipher.login.uri).toBe("ms-rd:subscribe?url=https://contoso.com");
});
it("should parse software license into secure notes", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(SoftwareLicenseTestData);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.SecureNote);
expect(cipher.name).toBe("software-license type");
expect(cipher.notes).toBe("someComment");
expect(cipher.secureNote).not.toBeNull();
expect(cipher.secureNote.type).toBe(SecureNoteType.Generic);
let customField = cipher.fields.find((f) => f.name === "IDS_LicenseProduct");
expect(customField).toBeDefined();
expect(customField.value).toEqual("someProduct");
customField = cipher.fields.find((f) => f.name === "IDS_LicenseVersion");
expect(customField).toBeDefined();
expect(customField.value).toEqual("someVersion");
customField = cipher.fields.find((f) => f.name === "IDS_LicenseName");
expect(customField).toBeDefined();
expect(customField.value).toEqual("some User");
customField = cipher.fields.find((f) => f.name === "IDS_LicenseKey");
expect(customField).toBeDefined();
expect(customField.value).toEqual("license-key");
customField = cipher.fields.find((f) => f.name === "IDS_LicenseAdditionalKey");
expect(customField).toBeDefined();
expect(customField.value).toEqual("additional-license-key");
customField = cipher.fields.find((f) => f.name === "IDS_LicenseURL");
expect(customField).toBeDefined();
expect(customField.value).toEqual("example.com");
customField = cipher.fields.find((f) => f.name === "IDS_LicenseProtected");
expect(customField).toBeDefined();
expect(customField.value).toEqual("1");
customField = cipher.fields.find((f) => f.name === "IDS_LicenseUserName");
expect(customField).toBeDefined();
expect(customField.value).toEqual("someUserName");
customField = cipher.fields.find((f) => f.name === "IDS_LicensePassword");
expect(customField).toBeDefined();
expect(customField.value).toEqual("somePassword");
customField = cipher.fields.find((f) => f.name === "IDS_LicensePurchaseDate");
expect(customField).toBeDefined();
const expectedDate = new Date("2025-05-12T00:00:00Z");
expect(customField.value).toEqual(expectedDate.toLocaleDateString());
customField = cipher.fields.find((f) => f.name === "IDS_LicenseOrderNumber");
expect(customField).toBeDefined();
expect(customField.value).toEqual("order number");
customField = cipher.fields.find((f) => f.name === "IDS_LicenseEmail");
expect(customField).toBeDefined();
expect(customField.value).toEqual("someEmail");
customField = cipher.fields.find((f) => f.name === "IDS_LicenseExpires");
expect(customField).toBeDefined();
expect(customField.value).toEqual("Nie");
});
it("should parse team viewer into login type", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(TeamViewerTestData);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.Login);
expect(cipher.name).toBe("TeamViewer type");
expect(cipher.notes).toBe("someNote");
expect(cipher.login).not.toBeNull();
expect(cipher.login.password).toBe("somePassword");
expect(cipher.login.username).toBe("");
expect(cipher.login.uri).toBe("partnerId");
const customField = cipher.fields.find((f) => f.name === "IDS_TeamViewerMode");
expect(customField).toBeDefined();
expect(customField.value).toEqual("0");
});
it("should parse putty into login type", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(PuttyTestData);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.Login);
expect(cipher.name).toBe("Putty type");
expect(cipher.notes).toBe("someNote");
expect(cipher.login).not.toBeNull();
expect(cipher.login.password).toBe("somePassword");
expect(cipher.login.username).toBe("someUser");
expect(cipher.login.uri).toBe("localhost");
let customField = cipher.fields.find((f) => f.name === "IDS_PuTTyProtocol");
expect(customField).toBeDefined();
expect(customField.value).toEqual("0");
customField = cipher.fields.find((f) => f.name === "IDS_PuTTyKeyFile");
expect(customField).toBeDefined();
expect(customField.value).toEqual("pathToKeyFile");
customField = cipher.fields.find((f) => f.name === "IDS_PuTTyKeyPassword");
expect(customField).toBeDefined();
expect(customField.value).toEqual("passwordForKeyFile");
customField = cipher.fields.find((f) => f.name === "IDS_PuTTyPort");
expect(customField).toBeDefined();
expect(customField.value).toEqual("8080");
});
it("should parse banking item type into login type", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(BankingTestData);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.Login);
expect(cipher.name).toBe("banking type");
expect(cipher.notes).toBe("someNote");
expect(cipher.login).not.toBeNull();
expect(cipher.login.password).toBe("somePassword");
expect(cipher.login.username).toBe("someUser");
expect(cipher.login.uri).toBe("http://some-bank.com");
let customField = cipher.fields.find((f) => f.name === "IDS_ECHolder");
expect(customField).toBeDefined();
expect(customField.value).toEqual("account holder");
customField = cipher.fields.find((f) => f.name === "IDS_ECAccountNumber");
expect(customField).toBeDefined();
expect(customField.value).toEqual("1234567890");
customField = cipher.fields.find((f) => f.name === "IDS_ECBLZ");
expect(customField).toBeDefined();
expect(customField.value).toEqual("12345678");
customField = cipher.fields.find((f) => f.name === "IDS_ECBankName");
expect(customField).toBeDefined();
expect(customField.value).toEqual("someBank");
customField = cipher.fields.find((f) => f.name === "IDS_ECBIC");
expect(customField).toBeDefined();
expect(customField.value).toEqual("bic");
customField = cipher.fields.find((f) => f.name === "IDS_ECIBAN");
expect(customField).toBeDefined();
expect(customField.value).toEqual("iban");
customField = cipher.fields.find((f) => f.name === "IDS_ECCardNumber");
expect(customField).toBeDefined();
expect(customField.value).toEqual("12345678");
customField = cipher.fields.find((f) => f.name === "IDS_ECPhone");
expect(customField).toBeDefined();
expect(customField.value).toEqual("0049");
customField = cipher.fields.find((f) => f.name === "IDS_ECLegitimacyID");
expect(customField).toBeDefined();
expect(customField.value).toEqual("1234");
customField = cipher.fields.find((f) => f.name === "IDS_ECPIN");
expect(customField).toBeDefined();
expect(customField.value).toEqual("123");
customField = cipher.fields.find((f) => f.name === "tan_1_value");
expect(customField).toBeDefined();
expect(customField.value).toEqual("1234");
customField = cipher.fields.find((f) => f.name === "tan_1_used");
expect(customField).toBeDefined();
expect(customField.value).toEqual("12.05.2025 15:10:54");
// TAN entries
customField = cipher.fields.find((f) => f.name === "tan_1_amount");
expect(customField).toBeDefined();
expect(customField.value).toEqual(" 100,00");
customField = cipher.fields.find((f) => f.name === "tan_1_comment");
expect(customField).toBeDefined();
expect(customField.value).toEqual("some TAN note");
customField = cipher.fields.find((f) => f.name === "tan_1_ccode");
expect(customField).toBeDefined();
expect(customField.value).toEqual("123");
customField = cipher.fields.find((f) => f.name === "tan_2_value");
expect(customField).toBeDefined();
expect(customField.value).toEqual("4321");
customField = cipher.fields.find((f) => f.name === "tan_2_amount");
expect(customField).toBeDefined();
expect(customField.value).toEqual(" 0,00");
});
it("should parse information into secure note type", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(InformationTestData);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.SecureNote);
expect(cipher.name).toBe("information type");
expect(cipher.notes).toBe("some note content");
});
it("should parse certificate into login type", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(CertificateTestData);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.Login);
expect(cipher.name).toBe("certificate type");
expect(cipher.notes).toBe("someNote");
expect(cipher.login).not.toBeNull();
expect(cipher.login.password).toBe("somePassword");
});
it("should parse encrypted file into login type", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(EncryptedFileTestData);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.Login);
expect(cipher.name).toBe("encrypted file type");
expect(cipher.notes).toBe("some comment");
expect(cipher.login).not.toBeNull();
expect(cipher.login.password).toBe("somePassword");
});
it("should parse document type into secure note", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(DocumentTestData);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.SecureNote);
expect(cipher.name).toBe("document type");
expect(cipher.notes).toBe("document comment");
let customField = cipher.fields.find((f) => f.name === "IDS_DocumentSize");
expect(customField).toBeDefined();
expect(customField.value).toEqual("27071");
customField = cipher.fields.find((f) => f.name === "IDS_DocumentFolder");
expect(customField).toBeDefined();
expect(customField.value).toEqual("C:\\Users\\DJSMI\\Downloads\\");
customField = cipher.fields.find((f) => f.name === "IDS_DocumentFile");
expect(customField).toBeDefined();
expect(customField.value).toEqual("C:\\Users\\DJSMI\\Downloads\\some.pdf");
});
it("should parse favourites and set them on the target item", async () => {
const importer = new PasswordDepot17XmlImporter();
const result = await importer.parse(PasswordTestData);
let cipher = result.ciphers.shift();
expect(cipher.name).toBe("password type");
expect(cipher.favorite).toBe(false);
cipher = result.ciphers.shift();
expect(cipher.name).toBe("password type (2)");
expect(cipher.favorite).toBe(true);
cipher = result.ciphers.shift();
expect(cipher.name).toBe("password type (3)");
expect(cipher.favorite).toBe(true);
});
it("should parse groups nodes into collections when importing into an organization", async () => {
const importer = new PasswordDepot17XmlImporter();
importer.organizationId = "someOrgId";
const collection = new CollectionView();
collection.name = "tempDB";
const actual = [collection];
const result = await importer.parse(PasswordTestData);
expect(result.collections).toEqual(actual);
});
});

View File

@@ -0,0 +1,500 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CipherType, FieldType, SecureNoteType } from "@bitwarden/common/vault/enums";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
import { ImportResult } from "../../models/import-result";
import { BaseImporter } from "../base-importer";
import { Importer } from "../importer";
import { PasswordDepotItemType, PasswordDepotCustomFieldType } from "./types";
/**
* Importer for Password Depot 17 xml files.
* @see https://www.password-depot.de/
* It provides methods to parse the XML data, extract relevant information, and create cipher objects
*/
export class PasswordDepot17XmlImporter extends BaseImporter implements Importer {
result = new ImportResult();
_favouritesLookupTable = new Set<string>();
// Parse the XML data from the Password Depot export file and extracts the relevant information
parse(data: string): Promise<ImportResult> {
const doc: XMLDocument = this.parseXml(data);
if (doc == null) {
this.result.success = false;
return Promise.resolve(this.result);
}
// Check if the root node is present
const rootNode = doc.querySelector("passwordfile");
if (rootNode == null) {
this.result.errorMessage = "Missing `passwordfile` node.";
this.result.success = false;
return Promise.resolve(this.result);
}
// Check if the version is supported
const headerNode = this.querySelectorDirectChild(rootNode, "header");
if (headerNode == null) {
this.result.success = false;
return Promise.resolve(this.result);
}
let versionNode = this.querySelectorDirectChild(headerNode, "version");
if (versionNode == null) {
// Adding a fallback for MacOS Password Depot 17.0 export files
// These files do not have a version node, but a dataformat node instead
versionNode = this.querySelectorDirectChild(headerNode, "dataformat");
if (versionNode == null) {
this.result.success = false;
return Promise.resolve(this.result);
}
}
const version = versionNode.textContent;
if (!version.startsWith("17")) {
this.result.errorMessage = "Unsupported export version detected - (only 17.0 is supported)";
this.result.success = false;
return Promise.resolve(this.result);
}
// Abort import if the file is encrypted
const encryptedNode = this.querySelectorDirectChild(headerNode, "encrypted");
if (encryptedNode != null && encryptedNode.textContent == "True") {
this.result.errorMessage = "Encrypted Password Depot files are not supported.";
this.result.success = false;
return Promise.resolve(this.result);
}
// Check if the passwords node is present
// This node contains all the password entries
const passwordsNode = rootNode.querySelector("passwords");
if (passwordsNode == null) {
this.result.errorMessage = "Missing `passwordfile > passwords` node.";
this.result.success = false;
return Promise.resolve(this.result);
}
this.buildFavouritesLookupTable(rootNode);
this.querySelectorAllDirectChild(passwordsNode, "group").forEach((group) => {
this.traverse(group, "");
});
if (this.organization) {
this.moveFoldersToCollections(this.result);
}
this.result.success = true;
return Promise.resolve(this.result);
}
// Traverses the XML tree and processes each node
// It starts from the root node and goes through each group and item
// This method is recursive and handles nested groups
private traverse(node: Element, groupPrefixName: string) {
const folderIndex = this.result.folders.length;
let groupName = groupPrefixName;
if (groupName !== "") {
groupName += "/";
}
// Check if the group has a fingerprint attribute (GUID of a folder)
const groupFingerprint = node.attributes.getNamedItem("fingerprint");
if (groupFingerprint?.textContent != "" && groupFingerprint.textContent != "null") {
const nameEl = node.attributes.getNamedItem("name");
groupName += nameEl == null ? "-" : nameEl.textContent;
const folder = new FolderView();
folder.name = groupName;
this.result.folders.push(folder);
}
this.querySelectorAllDirectChild(node, "item").forEach((entry) => {
const cipherIndex = this.result.ciphers.length;
const cipher = this.initLoginCipher();
//Set default item type similar how we default to Login in other importers
let sourceType: PasswordDepotItemType = PasswordDepotItemType.Password;
const entryFields = entry.children;
for (let i = 0; i < entryFields.length; i++) {
const entryField = entryFields[i];
// Skip processing historical entries
if (entryField.tagName === "hitems") {
continue;
}
if (entryField.tagName === "description") {
cipher.name = entryField.textContent;
continue;
}
if (entryField.tagName === "comment") {
cipher.notes = entryField.textContent;
continue;
}
if (entryField.tagName === "type") {
sourceType = entryField.textContent as PasswordDepotItemType;
switch (sourceType) {
case PasswordDepotItemType.Password:
case PasswordDepotItemType.RDP:
case PasswordDepotItemType.Putty:
case PasswordDepotItemType.TeamViewer:
case PasswordDepotItemType.Banking:
case PasswordDepotItemType.Certificate:
case PasswordDepotItemType.EncryptedFile:
cipher.type = CipherType.Login;
cipher.login = new LoginView();
break;
case PasswordDepotItemType.CreditCard:
cipher.type = CipherType.Card;
cipher.card = new CardView();
break;
case PasswordDepotItemType.SoftwareLicense:
case PasswordDepotItemType.Information:
case PasswordDepotItemType.Document:
cipher.type = CipherType.SecureNote;
cipher.secureNote = new SecureNoteView();
cipher.secureNote.type = SecureNoteType.Generic;
break;
case PasswordDepotItemType.Identity:
cipher.type = CipherType.Identity;
cipher.identity = new IdentityView();
break;
}
continue;
}
if (
sourceType === PasswordDepotItemType.Password ||
sourceType === PasswordDepotItemType.RDP ||
sourceType === PasswordDepotItemType.Putty ||
sourceType === PasswordDepotItemType.TeamViewer ||
sourceType === PasswordDepotItemType.Banking ||
sourceType === PasswordDepotItemType.Certificate ||
sourceType === PasswordDepotItemType.EncryptedFile
) {
if (this.parseLoginFields(entryField, cipher)) {
continue;
}
}
// fingerprint is the GUID of the entry
// Base on the previously parsed favourites, we can identify an entry and set the favorite flag accordingly
if (entryField.tagName === "fingerprint") {
if (this._favouritesLookupTable.has(entryField.textContent)) {
cipher.favorite = true;
}
}
if (entryField.tagName === "customfields") {
this.parseCustomFields(entryField, sourceType, cipher);
continue;
}
if (sourceType === PasswordDepotItemType.Banking && entryField.tagName === "tans") {
this.querySelectorAllDirectChild(entryField, "tan").forEach((tanEntry) => {
this.parseBankingTANs(tanEntry, cipher);
});
continue;
}
this.processKvp(cipher, entryField.tagName, entryField.textContent, FieldType.Text);
}
this.cleanupCipher(cipher);
this.result.ciphers.push(cipher);
if (groupName !== "") {
this.result.folderRelationships.push([cipherIndex, folderIndex]);
}
});
this.querySelectorAllDirectChild(node, "group").forEach((group) => {
this.traverse(group, groupName);
});
}
// Parses custom fields and adds them to the cipher
// It iterates through all the custom fields and adds them to the cipher
private parseCustomFields(
entryField: Element,
sourceType: PasswordDepotItemType,
cipher: CipherView,
) {
this.querySelectorAllDirectChild(entryField, "field").forEach((customField) => {
const customFieldObject = this.parseCustomField(customField);
if (customFieldObject == null) {
return;
}
switch (sourceType) {
case PasswordDepotItemType.CreditCard:
if (this.parseCreditCardCustomFields(customFieldObject, cipher)) {
return;
}
break;
case PasswordDepotItemType.Identity:
if (this.parseIdentityCustomFields(customFieldObject, cipher)) {
return;
}
break;
case PasswordDepotItemType.Information:
if (this.parseInformationCustomFields(customFieldObject, cipher)) {
return;
}
break;
default:
// For other types, we will process the custom field as a regular key-value pair
break;
}
this.processKvp(
cipher,
customFieldObject.name,
customFieldObject.value,
customFieldObject.type,
);
});
}
// Parses login fields and adds them to the cipher
private parseLoginFields(entryField: Element, cipher: CipherView): boolean {
if (entryField.tagName === "username") {
cipher.login.username = entryField.textContent;
return true;
}
if (entryField.tagName === "password") {
cipher.login.password = entryField.textContent;
return true;
}
if (entryField.tagName === "url") {
cipher.login.uris = this.makeUriArray(entryField.textContent);
return true;
}
return false;
}
// Parses a custom field and adds it to the cipher
private parseCustomField(customField: Element): FieldView | null {
let key: string = undefined;
let value: string = undefined;
let sourceFieldType: PasswordDepotCustomFieldType = PasswordDepotCustomFieldType.Memo;
let visible: string = undefined;
// A custom field is represented by a <field> element
// On exports from the Windows clients: It contains a <name>, <value>, and optionally a <type> and <visible> element
// On exports from the MacOs clients the key-values are defined as xml attributes instead of child nodes
if (customField.hasAttributes()) {
key = customField.getAttribute("name");
if (key == null) {
return null;
}
value = customField.getAttribute("value");
const typeAttr = customField.getAttribute("type");
sourceFieldType =
typeAttr != null
? (typeAttr as PasswordDepotCustomFieldType)
: PasswordDepotCustomFieldType.Memo;
visible = customField.getAttribute("visible");
} else {
const keyEl = this.querySelectorDirectChild(customField, "name");
key = keyEl != null ? keyEl.textContent : null;
if (key == null) {
return null;
}
const valueEl = this.querySelectorDirectChild(customField, "value");
value = valueEl != null ? valueEl.textContent : null;
const typeEl = this.querySelectorDirectChild(customField, "type");
sourceFieldType =
typeEl != null
? (typeEl.textContent as PasswordDepotCustomFieldType)
: PasswordDepotCustomFieldType.Memo;
const visibleEl = this.querySelectorDirectChild(customField, "visible");
visible = visibleEl != null ? visibleEl.textContent : null;
}
if (sourceFieldType === PasswordDepotCustomFieldType.Date) {
if (!isNaN(value as unknown as number)) {
// Convert excel date format to JavaScript date
const numericValue = parseInt(value);
const secondsInDay = 86400;
const missingLeapYearDays = secondsInDay * 1000;
value = new Date((numericValue - (25567 + 2)) * missingLeapYearDays).toLocaleDateString();
}
}
if (sourceFieldType === PasswordDepotCustomFieldType.Password) {
return { name: key, value: value, type: FieldType.Hidden, linkedId: null } as FieldView;
}
if (sourceFieldType === PasswordDepotCustomFieldType.Boolean) {
return { name: key, value: value, type: FieldType.Boolean, linkedId: null } as FieldView;
}
if (visible == "0") {
return { name: key, value: value, type: FieldType.Hidden, linkedId: null } as FieldView;
}
return { name: key, value: value, type: FieldType.Text, linkedId: null } as FieldView;
}
// Parses credit card fields and adds them to the cipher
private parseCreditCardCustomFields(customField: FieldView, cipher: CipherView): boolean {
if (customField.name === "IDS_CardHolder") {
cipher.card.cardholderName = customField.value;
return true;
}
if (customField.name === "IDS_CardNumber") {
cipher.card.number = customField.value;
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
return true;
}
if (customField.name === "IDS_CardExpires") {
this.setCardExpiration(cipher, customField.value);
return true;
}
if (customField.name === "IDS_CardCode") {
cipher.card.code = customField.value;
return true;
}
return false;
}
// Parses identity fields and adds them to the cipher
private parseIdentityCustomFields(customField: FieldView, cipher: CipherView): boolean {
if (customField.name === "IDS_IdentityName") {
this.processFullName(cipher, customField.value);
return true;
}
if (customField.name === "IDS_IdentityEmail") {
cipher.identity.email = customField.value;
return true;
}
if (customField.name === "IDS_IdentityFirstName") {
cipher.identity.firstName = customField.value;
return true;
}
if (customField.name === "IDS_IdentityLastName") {
cipher.identity.lastName = customField.value;
return true;
}
if (customField.name === "IDS_IdentityCompany") {
cipher.identity.company = customField.value;
return true;
}
if (customField.name === "IDS_IdentityAddress1") {
cipher.identity.address1 = customField.value;
return true;
}
if (customField.name === "IDS_IdentityAddress2") {
cipher.identity.address2 = customField.value;
return true;
}
if (customField.name === "IDS_IdentityCity") {
cipher.identity.city = customField.value;
return true;
}
if (customField.name === "IDS_IdentityState") {
cipher.identity.state = customField.value;
return true;
}
if (customField.name === "IDS_IdentityZIP") {
cipher.identity.postalCode = customField.value;
return true;
}
if (customField.name === "IDS_IdentityCountry") {
cipher.identity.country = customField.value;
return true;
}
if (customField.name === "IDS_IdentityPhone") {
cipher.identity.phone = customField.value;
return true;
}
return false;
}
// Parses information custom fields and adds them to the cipher
private parseInformationCustomFields(customField: FieldView, cipher: CipherView): boolean {
if (customField.name === "IDS_InformationText") {
cipher.notes = customField.value;
return true;
}
return false;
}
// Parses TAN objects and adds them to the cipher
// It iterates through all the TAN fields and adds them to the cipher
private parseBankingTANs(TANsField: Element, cipher: CipherView) {
let tanNumber = "0";
const entryFields = TANsField.children;
for (let i = 0; i < entryFields.length; i++) {
const entryField = entryFields[i];
if (entryField.tagName === "number") {
tanNumber = entryField.textContent;
continue;
}
this.processKvp(cipher, `tan_${tanNumber}_${entryField.tagName}`, entryField.textContent);
}
}
// Parses the favourites-node from the XML file, which contains a base64 encoded string
// The string contains the fingerprints/GUIDs of the favourited entries, separated by new lines
private buildFavouritesLookupTable(rootNode: Element): void {
const favouritesNode = this.querySelectorDirectChild(rootNode, "favorites");
if (favouritesNode == null) {
return;
}
const decodedBase64String = atob(favouritesNode.textContent);
if (decodedBase64String.indexOf("\r\n") > 0) {
decodedBase64String.split("\r\n").forEach((line) => {
this._favouritesLookupTable.add(line);
});
return;
}
decodedBase64String.split("\n").forEach((line) => {
this._favouritesLookupTable.add(line);
});
}
}

View File

@@ -0,0 +1,2 @@
export * from "./password-depot-item-type";
export * from "./password-depot-custom-field-type";

View File

@@ -0,0 +1,15 @@
/** This object represents the different custom field types in Password Depot */
export const PasswordDepotCustomFieldType = Object.freeze({
Password: "1",
Memo: "2",
Date: "3",
Number: "4",
Boolean: "5",
Decimal: "6",
Email: "7",
URL: "8",
} as const);
/** This type represents the different custom field types in Password Depot */
export type PasswordDepotCustomFieldType =
(typeof PasswordDepotCustomFieldType)[keyof typeof PasswordDepotCustomFieldType];

View File

@@ -0,0 +1,19 @@
/** This object represents the different item types in Password Depot */
export const PasswordDepotItemType = Object.freeze({
Password: "0",
CreditCard: "1",
SoftwareLicense: "2",
Identity: "3",
Information: "4",
Banking: "5",
EncryptedFile: "6",
Document: "7",
RDP: "8",
Putty: "9",
TeamViewer: "10",
Certificate: "11",
} as const);
/** This type represents the different item types in Password Depot */
export type PasswordDepotItemType =
(typeof PasswordDepotItemType)[keyof typeof PasswordDepotItemType];

View File

@@ -0,0 +1,283 @@
export const BankingTestData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/17.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
<group name="tempDB" fingerprint="CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D">
<item>
<description>banking type</description>
<type>5</type>
<password>somePassword</password>
<username>someUser</username>
<url>some-bank.com</url>
<comment>someNote</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:11:29</lastmodified>
<expirydate fmt="dd.mm.yyyy">02.05.2027</expirydate>
<importance>1</importance>
<fingerprint>DEB91652-52C6-402E-9D44-3557829BC6DF</fingerprint>
<template>&lt;USER&gt;&lt;TAB&gt;&lt;PASS&gt;&lt;ENTER&gt;</template>
<imageindex>127</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr></paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:09:17</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:09:17</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags></tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
<customfields>
<field>
<name>IDS_ECHolder</name>
<value>account holder</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECAccountNumber</name>
<value>1234567890</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECBLZ</name>
<value>12345678</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECBankName</name>
<value>someBank</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECBIC</name>
<value>bic</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECIBAN</name>
<value>iban</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECCardNumber</name>
<value>12345678</value>
<visible>-1</visible>
<kind>0</kind>
<type>10</type>
</field>
<field>
<name>IDS_ECPhone</name>
<value>0049</value>
<visible>0</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECLegitimacyID</name>
<value>1234</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECPIN</name>
<value>123</value>
<visible>0</visible>
<kind>0</kind>
<type>1</type>
</field>
</customfields>
<tans>
<tan>
<number>1</number>
<value>1234</value>
<used fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:10:54</used>
<amount> 100,00</amount>
<comment>some TAN note</comment>
<ccode>123</ccode>
</tan>
<tan>
<number>2</number>
<value>4321</value>
<amount> 0,00</amount>
<comment></comment>
<ccode></ccode>
</tan>
</tans>
<hitems>
<hitem>
<description>banking type</description>
<type>5</type>
<password>somePassword</password>
<username>someUser</username>
<url>some-bank.com</url>
<comment>someNote</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:10:35</lastmodified>
<expirydate fmt="dd.mm.yyyy">02.05.2027</expirydate>
<importance>1</importance>
<fingerprint>DEB91652-52C6-402E-9D44-3557829BC6DF</fingerprint>
<template>&lt;USER&gt;&lt;TAB&gt;&lt;PASS&gt;&lt;ENTER&gt;</template>
<imageindex>127</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr></paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:09:17</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:09:17</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags></tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
<customfields>
<field>
<name>IDS_ECHolder</name>
<value>account holder</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECAccountNumber</name>
<value>1234567890</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECBLZ</name>
<value>12345678</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECBankName</name>
<value>someBank</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECBIC</name>
<value>bic</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECIBAN</name>
<value>iban</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECCardNumber</name>
<value>12345678</value>
<visible>-1</visible>
<kind>0</kind>
<type>10</type>
</field>
<field>
<name>IDS_ECPhone</name>
<value>0049</value>
<visible>0</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECLegitimacyID</name>
<value>1234</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_ECPIN</name>
<value>123</value>
<visible>0</visible>
<kind>0</kind>
<type>1</type>
</field>
</customfields>
</hitem>
</hitems>
</item>
</group>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo=
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg==
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,76 @@
export const CertificateTestData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/17.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
<group name="tempDB" fingerprint="CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D">
<item>
<description>certificate type</description>
<type>11</type>
<password>somePassword</password>
<username></username>
<url></url>
<comment>someNote</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:15:57</lastmodified>
<expirydate fmt="dd.mm.yyyy">00.00.0000</expirydate>
<importance>1</importance>
<fingerprint>21288702-B042-46D9-9DDF-B44A5CD04A72</fingerprint>
<template></template>
<imageindex>130</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr></paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:15:26</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:15:26</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags>someTag</tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
</item>
</group>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo=
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg==
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,148 @@
export const CreditCardTestData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/17.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
<group name="tempDB" fingerprint="CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D">
<item>
<description>some CreditCard</description>
<type>1</type>
<password>4222422242224222</password>
<username>some CC holder</username>
<url></url>
<comment>someComment</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">08.05.2025 12:09:47</lastmodified>
<expirydate fmt="dd.mm.yyyy">08.05.2026</expirydate>
<importance>1</importance>
<fingerprint>DD9B52F8-B2CE-42C2-A891-5E20DA23EA20</fingerprint>
<template></template>
<imageindex>126</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr></paramstr>
<category></category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">08.05.2025 12:08:48</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">08.05.2025 12:08:48</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags></tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
<customfields>
<field>
<name>IDS_CardType</name>
<value>0</value>
<visible>0</visible>
<kind>0</kind>
<type>4</type>
</field>
<field>
<name>IDS_CardHolder</name>
<value>some CC holder</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_CardNumber</name>
<value>4222422242224222</value>
<visible>-1</visible>
<kind>0</kind>
<type>10</type>
</field>
<field>
<name>IDS_CardExpires</name>
<value>05/2026</value>
<visible>-1</visible>
<kind>0</kind>
<type>3</type>
</field>
<field>
<name>IDS_CardCode</name>
<value>123</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_CardPhone</name>
<value></value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_CardURL</name>
<value></value>
<visible>-1</visible>
<kind>0</kind>
<type>8</type>
</field>
<field>
<name>IDS_CardAdditionalCode</name>
<value></value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_CardAdditionalInfo</name>
<value></value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_CardPIN</name>
<value>123</value>
<visible>-1</visible>
<kind>0</kind>
<type>1</type>
</field>
</customfields>
</item>
</group>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo=
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg==
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,99 @@
export const DocumentTestData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/17.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
<group name="tempDB" fingerprint="CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D">
<item>
<description>document type</description>
<type>7</type>
<password></password>
<username></username>
<url></url>
<comment>document comment</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">03.06.2025 17:45:30</lastmodified>
<expirydate fmt="dd.mm.yyyy">00.00.0000</expirydate>
<importance>1</importance>
<fingerprint>1B8E7F2C-9229-43C6-AB89-42101809C822</fingerprint>
<template></template>
<imageindex>133</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr></paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">05.06.2025 21:49:49</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags></tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
<customfields>
<field>
<name>IDS_DocumentSize</name>
<value>27071</value>
<visible>0</visible>
<kind>0</kind>
<type>4</type>
</field>
<field>
<name>IDS_DocumentFolder</name>
<value>C:\\Users\\DJSMI\\Downloads\\</value>
<visible>0</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_DocumentFile</name>
<value>C:\\Users\\DJSMI\\Downloads\\some.pdf</value>
<visible>0</visible>
<kind>0</kind>
<type>0</type>
</field>
</customfields>
</item>
</group>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo=
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg==
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,76 @@
export const EncryptedFileTestData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/17.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
<group name="tempDB" fingerprint="CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D">
<item>
<description>encrypted file type</description>
<type>6</type>
<password>somePassword</password>
<username></username>
<url></url>
<comment>some comment</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:15:17</lastmodified>
<expirydate fmt="dd.mm.yyyy">00.00.0000</expirydate>
<importance>1</importance>
<fingerprint>E4CA245D-A326-4359-9488-CC207B33C6C0</fingerprint>
<template></template>
<imageindex>132</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr></paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:14:58</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:14:58</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags></tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
</item>
</group>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo=
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg==
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,197 @@
export const IdentityTestData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/17.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
<group name="tempDB" fingerprint="CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D">
<item>
<description>identity type</description>
<type>3</type>
<password></password>
<username>account-name/id</username>
<url>website</url>
<comment>someNote</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:14:33</lastmodified>
<expirydate fmt="dd.mm.yyyy">00.00.0000</expirydate>
<importance>1</importance>
<fingerprint>0E6085E9-7560-4826-814E-EFE1724E1377</fingerprint>
<template></template>
<imageindex>129</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr></paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:12:52</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:12:52</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags></tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
<customfields>
<field>
<name>IDS_IdentityName</name>
<value>account-name/id</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityEmail</name>
<value>email</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityFirstName</name>
<value>firstName</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityLastName</name>
<value>surName</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityCompany</name>
<value>someCompany</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityAddress1</name>
<value>someStreet</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityAddress2</name>
<value>address 2</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityCity</name>
<value>town</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityState</name>
<value>state</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityZIP</name>
<value>zipCode</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityCountry</name>
<value>country</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityPhone</name>
<value>phoneNumber</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityWebsite</name>
<value>website</value>
<visible>-1</visible>
<kind>0</kind>
<type>8</type>
</field>
<field>
<name>IDS_IdentityBirthDate</name>
<value>45789</value>
<visible>-1</visible>
<kind>0</kind>
<type>3</type>
</field>
<field>
<name>IDS_IdentityMobile</name>
<value>mobileNumber</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityFax</name>
<value>faxNumber</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_IdentityHouseNumber</name>
<value>123</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
</customfields>
</item>
</group>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo=
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg==
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,19 @@
export { InvalidRootNodeData } from "./missing-root-node.xml";
export { MissingPasswordsNodeData } from "./missing-passwords-node.xml";
export { InvalidVersionData } from "./wrong-version.xml";
export { EncryptedFileData } from "./noop-encrypted-file.xml";
export { PasswordTestData } from "./password.xml";
export { CreditCardTestData } from "./credit-card.xml";
export { IdentityTestData } from "./identity.xml";
export { RDPTestData } from "./rdp.xml";
export { SoftwareLicenseTestData } from "./software-license.xml";
export { TeamViewerTestData } from "./team-viewer.xml";
export { PuttyTestData } from "./putty.xml";
export { BankingTestData } from "./banking.xml";
export { InformationTestData } from "./information.xml";
export { CertificateTestData } from "./certificate.xml";
export { EncryptedFileTestData } from "./encrypted-file.xml";
export { DocumentTestData } from "./document.xml";
export { MacOS_WrongVersion } from "./macos-wrong-version.xml";
export { MacOS_PasswordDepotXmlFile } from "./macos-customfields.xml";
export { MacOS_MultipleFolders } from "./macos-multiple-folders.xml";

View File

@@ -0,0 +1,85 @@
export const InformationTestData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/17.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
<group name="tempDB" fingerprint="CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D">
<item>
<description>information type</description>
<type>4</type>
<password></password>
<username></username>
<url></url>
<comment></comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:14:54</lastmodified>
<expirydate fmt="dd.mm.yyyy">00.00.0000</expirydate>
<importance>1</importance>
<fingerprint>546AFAE7-6F64-4040-838B-AFE691580356</fingerprint>
<template></template>
<imageindex>131</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr></paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:14:39</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:14:39</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags></tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
<customfields>
<field>
<name>IDS_InformationText</name>
<value>some note content</value>
<visible>0</visible>
<kind>0</kind>
<type>2</type>
</field>
</customfields>
</item>
</group>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo=
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg==
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,42 @@
export const MacOS_PasswordDepotXmlFile = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/12.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<encrypted>0</encrypted>
<compressed>0</compressed>
<authtype>0</authtype>
<enctype>1</enctype>
<kdftype>1</kdftype>
<dataformat>17</dataformat>
<lastsynctime FMT="dd.MM.yyyy hh:mm:ss">30.12.1899 00:00:00</lastsynctime>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">23.06.2025 16:30:50</lastmodified>
<datetime FMT="dd.MM.yyyy hh:mm:ss">25.06.2025 14:30:47</datetime>
<fingerprint>2C1A154A-3BB0-4871-9537-3634DE303F8E</fingerprint>
<maxhistory>7</maxhistory>
</header>
<passwords>
<group name="null" fingerprint="null">
<item>
<description>card 1</description>
<type>1</type>
<comment>comment</comment>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">23.06.2025 16:14:33</lastmodified>
<fingerprint>EBF4AC3D-86C9-49BE-826B-BAE5FF9E3575</fingerprint>
<created FMT="dd.MM.yyyy hh:mm:ss">23.06.2025 16:13:40</created>
<lastaccessed FMT="dd.MM.yyyy hh:mm:ss">25.06.2025 14:17:10</lastaccessed>
<customfields>
<field name="IDS_CardType" value="2" />
<field name="IDS_CardHolder" value="some CC holder" visible="1" />
<field name="IDS_CardNumber" value="4242424242424242" visible="1" />
<field name="IDS_CardExpires" value="8/2028" visible="1" />
<field name="IDS_CardCode" value="125" visible="1" />
<field name="IDS_CardPhone" visible="1" />
<field name="IDS_CardURL" value="fede.com" visible="1" />
<field name="IDS_CardAdditionalCode" visible="1" />
<field name="IDS_CardAdditionalInfo" visible="1" />
<field name="IDS_CardPIN" value="1122" visible="1" />
</customfields>
</item>
</group>
</passwords>
</passwordfile>`;

View File

@@ -0,0 +1,215 @@
export const MacOS_MultipleFolders = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/12.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<encrypted>0</encrypted>
<compressed>0</compressed>
<authtype>0</authtype>
<enctype>1</enctype>
<kdftype>1</kdftype>
<dataformat>17</dataformat>
<lastsynctime FMT="dd.MM.yyyy hh:mm:ss">30.12.1899 00:00:00</lastsynctime>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">27.06.2025 10:39:07</lastmodified>
<datetime FMT="dd.MM.yyyy hh:mm:ss">27.06.2025 10:39:27</datetime>
<fingerprint>7DCDD3FA-F512-4CD4-AEED-DE2A4C8375CF</fingerprint>
<maxhistory>7</maxhistory>
</header>
<passwords>
<group name="null" fingerprint="null">
<item>
<description>remote desktop</description>
<type>8</type>
<password>pass</password>
<username>username</username>
<url>compjter</url>
<comment>comment</comment>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:04:57</lastmodified>
<fingerprint>81316050-9B9C-4D9B-9549-45B52A0BE6BB</fingerprint>
<template>&lt;USER&gt;&lt;TAB&gt;&lt;PASS&gt;&lt;ENTER&gt;</template>
<category>Private</category>
<created FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:04:32</created>
<lastaccessed FMT="dd.MM.yyyy hh:mm:ss">27.06.2025 10:28:02</lastaccessed>
<tags>tgmac</tags>
</item>
<item>
<description>teamviewer</description>
<type>10</type>
<password>pass</password>
<url>partnerid</url>
<comment>comment</comment>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:05:28</lastmodified>
<expirydate FMT="dd.MM.yyyy">26.06.2025</expirydate>
<fingerprint>8AAACC16-4FD4-4E52-9C1F-6979B051B510</fingerprint>
<category>Internet</category>
<created FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:05:03</created>
<lastaccessed FMT="dd.MM.yyyy hh:mm:ss">27.06.2025 10:28:02</lastaccessed>
<tags>tag</tags>
<customfields>
<field name="IDS_TeamViewerMode" value="0" visible="1" />
</customfields>
</item>
<item>
<description>ec card</description>
<type>5</type>
<password>pass</password>
<username>user</username>
<url>url</url>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:08:35</lastmodified>
<fingerprint>B5C148A4-C408-427C-B69C-F88E7C529FA4</fingerprint>
<template>&lt;USER&gt;&lt;TAB&gt;&lt;PASS&gt;&lt;ENTER&gt;</template>
<created FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:08:00</created>
<lastaccessed FMT="dd.MM.yyyy hh:mm:ss">27.06.2025 10:28:02</lastaccessed>
<customfields>
<field name="IDS_ECHolder" value="holder" visible="1" />
<field name="IDS_ECAccountNumber" value="account" visible="1" />
<field name="IDS_ECBLZ" value="bank" visible="1" />
<field name="IDS_ECBankName" value="bank name" visible="1" />
<field name="IDS_ECBIC" value="bic" visible="1" />
<field name="IDS_ECIBAN" value="iban" visible="1" />
<field name="IDS_ECCardNumber" value="6464646464646464" visible="1" />
<field name="IDS_ECPhone" value="1324124" />
<field name="IDS_ECLegitimacyID" value="leg id" visible="1" />
<field name="IDS_ECPIN" value="1234" />
</customfields>
</item>
<item>
<description>identity</description>
<type>3</type>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:09:50</lastmodified>
<fingerprint>87574AD4-8844-4A01-9381-AFF0907198A3</fingerprint>
<created FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:09:19</created>
<lastaccessed FMT="dd.MM.yyyy hh:mm:ss">27.06.2025 10:28:02</lastaccessed>
<customfields>
<field name="IDS_IdentityName" value="name" visible="1" />
<field name="IDS_IdentityEmail" value="email" visible="1" />
<field name="IDS_IdentityFirstName" value="first" visible="1" />
<field name="IDS_IdentityLastName" value="last" visible="1" />
<field name="IDS_IdentityCompany" value="compant" visible="1" />
<field name="IDS_IdentityAddress1" value="street" visible="1" />
<field name="IDS_IdentityAddress2" value="address" visible="1" />
<field name="IDS_IdentityCity" value="city" visible="1" />
<field name="IDS_IdentityState" value="state" visible="1" />
<field name="IDS_IdentityZIP" value="zip" visible="1" />
<field name="IDS_IdentityCountry" value="country" visible="1" />
<field name="IDS_IdentityPhone" value="phone" visible="1" />
<field name="IDS_IdentityWebsite" value="web" visible="1" />
<field name="IDS_IdentityBirthDate" visible="1" />
<field name="IDS_IdentityMobile" visible="1" />
<field name="IDS_IdentityFax" visible="1" />
<field name="IDS_IdentityHouseNumber" visible="1" />
</customfields>
</item>
<item>
<description>credit card</description>
<type>1</type>
<comment>comment</comment>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 19:07:38</lastmodified>
<fingerprint>E98E3CBA-1578-48AD-8E41-CFD3280045BB</fingerprint>
<created FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:06:32</created>
<lastaccessed FMT="dd.MM.yyyy hh:mm:ss">27.06.2025 10:28:02</lastaccessed>
<customfields>
<field name="IDS_CardType" value="0" />
<field name="IDS_CardHolder" value="holder" visible="1" />
<field name="IDS_CardNumber" value="525252525252525" visible="1" />
<field name="IDS_CardExpires" value="6/2028" visible="1" />
<field name="IDS_CardCode" value="333" visible="1" />
<field name="IDS_CardPhone" value="1234324" visible="1" />
<field name="IDS_CardURL" value="url" visible="1" />
<field name="IDS_CardAdditionalCode" value="code" visible="1" />
<field name="IDS_CardAdditionalInfo" value="addinfo" visible="1" />
<field name="IDS_CardPIN" value="111" visible="1" />
</customfields>
</item>
<group name="folder macos" fingerprint="BF823CF6-B9D1-49FD-AEB9-4F59BDD8DBB5"
imageindex="-1" imagecustom="0" category="null" comments="null">
<item>
<description>password</description>
<password>passmac</password>
<username>usernam</username>
<comment>comment</comment>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:04:30</lastmodified>
<fingerprint>DE8AD61B-8EC0-4E72-9BC8-971E80712B50</fingerprint>
<template>&lt;USER&gt;&lt;TAB&gt;&lt;PASS&gt;&lt;ENTER&gt;</template>
<category>General</category>
<created FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:04:04</created>
<lastaccessed FMT="dd.MM.yyyy hh:mm:ss">27.06.2025 10:28:02</lastaccessed>
<tags>tagmac</tags>
</item>
<item>
<description>informationb</description>
<type>4</type>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:10:01</lastmodified>
<fingerprint>7E9E6941-BB3B-47F2-9E43-33F900EBBF95</fingerprint>
<category>Banking</category>
<created FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:09:53</created>
<lastaccessed FMT="dd.MM.yyyy hh:mm:ss">27.06.2025 10:28:02</lastaccessed>
<customfields>
<field name="IDS_InformationText" value="content" />
</customfields>
</item>
<item>
<description>certificat</description>
<type>11</type>
<password>passsss</password>
<comment>comment</comment>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:10:28</lastmodified>
<expirydate FMT="dd.MM.yyyy">26.06.2025</expirydate>
<fingerprint>1F36748F-0374-445E-B020-282EAE26259F</fingerprint>
<category>Internet</category>
<created FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:10:10</created>
<lastaccessed FMT="dd.MM.yyyy hh:mm:ss">27.06.2025 10:28:02</lastaccessed>
<tags>tag</tags>
</item>
</group>
<group name="folder 2" fingerprint="84E728B7-67AC-47D7-9F8C-4335905A4087"
imageindex="-1" imagecustom="0" category="null" comments="null">
<item>
<description>putty</description>
<type>9</type>
<password>pass</password>
<username>username</username>
<url>host</url>
<comment>comment</comment>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:06:08</lastmodified>
<expirydate FMT="dd.MM.yyyy">26.06.2025</expirydate>
<fingerprint>7947A949-98F0-4F26-BE12-5FFAFE7601C8</fingerprint>
<template>&lt;USER&gt;&lt;TAB&gt;&lt;PASS&gt;&lt;ENTER&gt;</template>
<category>Banking</category>
<created FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:05:38</created>
<lastaccessed FMT="dd.MM.yyyy hh:mm:ss">27.06.2025 10:28:02</lastaccessed>
<tags>tag</tags>
<customfields>
<field name="IDS_PuTTyProtocol" value="0" visible="1" />
<field name="IDS_PuTTyKeyFile" value="keyfile" visible="1" />
<field name="IDS_PuTTyKeyPassword" value="keypass" visible="1" />
<field name="IDS_PuTTyPort" value="port" visible="1" />
</customfields>
</item>
<item>
<description>soft license</description>
<type>2</type>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:09:02</lastmodified>
<fingerprint>2A5CF4C1-70D0-4F27-A1DE-4CFEF5FB71CF</fingerprint>
<created FMT="dd.MM.yyyy hh:mm:ss">26.06.2025 16:08:43</created>
<lastaccessed FMT="dd.MM.yyyy hh:mm:ss">27.06.2025 10:28:02</lastaccessed>
<customfields>
<field name="IDS_LicenseProduct" value="prod" />
<field name="IDS_LicenseVersion" value="version" />
<field name="IDS_LicenseName" value="registered" visible="1" />
<field name="IDS_LicenseKey" visible="1" />
<field name="IDS_LicenseAdditionalKey" visible="1" />
<field name="IDS_LicenseURL" value="url" visible="1" />
<field name="IDS_LicenseProtected" value="protected" />
<field name="IDS_LicenseUserName" value="username" visible="1" />
<field name="IDS_LicensePassword" value="pass" visible="1" />
<field name="IDS_LicensePurchaseDate" value="2147483647" />
<field name="IDS_LicenseOrderNumber"
value="ccbdflbunjrcnilnerjnekirlfvefkbfinhkegljd" visible="1" />
<field name="IDS_LicenseEmail" value="ccc" visible="1" />
<field name="IDS_LicenseExpires" />
</customfields>
</item>
</group>
</group>
</passwords>
</passwordfile>`;

View File

@@ -0,0 +1,21 @@
export const MacOS_WrongVersion = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/12.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<encrypted>0</encrypted>
<compressed>0</compressed>
<authtype>0</authtype>
<enctype>1</enctype>
<kdftype>1</kdftype>
<dataformat>18</dataformat>
<lastsynctime FMT="dd.MM.yyyy hh:mm:ss">30.12.1899 00:00:00</lastsynctime>
<lastmodified FMT="dd.MM.yyyy hh:mm:ss">23.06.2025 16:30:50</lastmodified>
<datetime FMT="dd.MM.yyyy hh:mm:ss">25.06.2025 14:30:47</datetime>
<fingerprint>2C1A154A-3BB0-4871-9537-3634DE303F8E</fingerprint>
<maxhistory>7</maxhistory>
</header>
<passwords>
<group name="null" fingerprint="null">
</group>
</passwords>
</passwordfile>`;

View File

@@ -0,0 +1,25 @@
export const MissingPasswordsNodeData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/18.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,28 @@
export const InvalidRootNodeData = `<?xml version="1.0" encoding="utf-8"?>
<invalidRootNode xmlns="https://www.password-depot.de/schemas/passwordfile/18.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
</categories>
</invalidRootNode>`;

View File

@@ -0,0 +1,27 @@
export const EncryptedFileData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/17.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>True</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,222 @@
export const PasswordTestData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/17.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
<group name="tempDB" fingerprint="CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D">
<item>
<description>password type</description>
<type>0</type>
<password>p6J&lt;]fmjv!:H&amp;iJ7/Mwt@3i8</password>
<username>someUser</username>
<url>example.com</url>
<comment>someComment</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">07.05.2025 13:37:56</lastmodified>
<expirydate fmt="dd.mm.yyyy">07.05.2025</expirydate>
<importance>0</importance>
<fingerprint>27ACAC2D-8DDA-4088-8D3A-E6C5F40ED46E</fingerprint>
<template>&lt;USER&gt;&lt;TAB&gt;&lt;PASS&gt;&lt;ENTER&gt;</template>
<imageindex>0</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr></paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">07.05.2025 13:36:48</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">07.05.2025 13:36:48</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags>someTag</tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
<customfields>
<field>
<name>passwort</name>
<value>password</value>
<visible>-1</visible>
<kind>0</kind>
<type>1</type>
</field>
<field>
<name>memo</name>
<value>memo</value>
<visible>-1</visible>
<kind>0</kind>
<type>2</type>
</field>
<field>
<name>datum</name>
<value>45790</value>
<visible>-1</visible>
<kind>0</kind>
<type>3</type>
</field>
<field>
<name>nummer</name>
<value>1</value>
<visible>-1</visible>
<kind>0</kind>
<type>4</type>
</field>
<field>
<name>boolean</name>
<value>1</value>
<visible>-1</visible>
<kind>0</kind>
<type>5</type>
</field>
<field>
<name>decimal</name>
<value>1,01</value>
<visible>-1</visible>
<kind>0</kind>
<type>6</type>
</field>
<field>
<name>email</name>
<value>who@cares.com</value>
<visible>-1</visible>
<kind>0</kind>
<type>7</type>
</field>
<field>
<name>url</name>
<value>example.com</value>
<visible>-1</visible>
<kind>0</kind>
<type>8</type>
</field>
</customfields>
</item>
<item>
<description>password type (2)</description>
<type>0</type>
<password>p6J&lt;]fmjv!:H&amp;iJ7/Mwt@3i8</password>
<username>someUser</username>
<url></url>
<comment>someComment</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">07.05.2025 13:37:56</lastmodified>
<expirydate fmt="dd.mm.yyyy">07.05.2025</expirydate>
<importance>0</importance>
<fingerprint>AF74FF86-FE39-4584-8E96-FE950C289DF8</fingerprint>
<template>&lt;USER&gt;&lt;TAB&gt;&lt;PASS&gt;&lt;ENTER&gt;</template>
<imageindex>0</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr></paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">07.05.2025 13:36:48</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">07.05.2025 13:36:48</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags>someTag</tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
</item>
<item>
<description>password type (3)</description>
<type>0</type>
<password>p6J&lt;]fmjv!:H&amp;iJ7/Mwt@3i8</password>
<username>someUser</username>
<url></url>
<comment>someComment</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">07.05.2025 13:37:56</lastmodified>
<expirydate fmt="dd.mm.yyyy">07.05.2025</expirydate>
<importance>0</importance>
<fingerprint>BF74FF86-FA39-4584-8E96-FA950C249DF8</fingerprint>
<template>&lt;USER&gt;&lt;TAB&gt;&lt;PASS&gt;&lt;ENTER&gt;</template>
<imageindex>0</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr></paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">07.05.2025 13:36:48</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">07.05.2025 13:36:48</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags>someTag</tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
</item>
</group>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4CkJGNzRGRjg2LUZBMzktNDU4NC04RTk2LUZBOTUwQzI0OURGOA==
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg==
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,106 @@
export const PuttyTestData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/17.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
<group name="tempDB" fingerprint="CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D">
<item>
<description>Putty type</description>
<type>9</type>
<password>somePassword</password>
<username>someUser</username>
<url>localhost</url>
<comment>someNote</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:09:09</lastmodified>
<expirydate fmt="dd.mm.yyyy">00.00.0000</expirydate>
<importance>1</importance>
<fingerprint>32207D79-B70B-4987-BC73-3F7AD75D2C63</fingerprint>
<template></template>
<imageindex>125</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr>cli command</paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:08:18</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:08:18</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags>someTag</tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
<customfields>
<field>
<name>IDS_PuTTyProtocol</name>
<value>0</value>
<visible>0</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_PuTTyKeyFile</name>
<value>pathToKeyFile</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_PuTTyKeyPassword</name>
<value>passwordForKeyFile</value>
<visible>-1</visible>
<kind>0</kind>
<type>1</type>
</field>
<field>
<name>IDS_PuTTyPort</name>
<value>8080</value>
<visible>-1</visible>
<kind>0</kind>
<type>4</type>
</field>
</customfields>
</item>
</group>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo=
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg==
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,76 @@
export const RDPTestData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/17.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
<group name="tempDB" fingerprint="CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D">
<item>
<description>rdp type</description>
<type>8</type>
<password>somePassword</password>
<username>someUser</username>
<url>ms-rd:subscribe?url=https://contoso.com</url>
<comment>someNote</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:07:33</lastmodified>
<expirydate fmt="dd.mm.yyyy">12.05.2025</expirydate>
<importance>1</importance>
<fingerprint>24CFF328-3036-48E3-99A3-85CD337725D3</fingerprint>
<template></template>
<imageindex>123</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr>commandline command</paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:06:24</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:06:24</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags>sometag</tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
</item>
</group>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo=
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg==
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,169 @@
export const SoftwareLicenseTestData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/17.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
<group name="tempDB" fingerprint="CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D">
<item>
<description>software-license type</description>
<type>2</type>
<password>somePassword</password>
<username>someUserName</username>
<url>example.com</url>
<comment>someComment</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:12:48</lastmodified>
<expirydate fmt="dd.mm.yyyy">00.00.0000</expirydate>
<importance>1</importance>
<fingerprint>220206EB-BE82-4E78-8FFB-9316D854721F</fingerprint>
<template></template>
<imageindex>128</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr></paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:11:33</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:11:33</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags></tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
<customfields>
<field>
<name>IDS_LicenseProduct</name>
<value>someProduct</value>
<visible>0</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_LicenseVersion</name>
<value>someVersion</value>
<visible>0</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_LicenseName</name>
<value>some User</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_LicenseKey</name>
<value>license-key</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_LicenseAdditionalKey</name>
<value>additional-license-key</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_LicenseURL</name>
<value>example.com</value>
<visible>-1</visible>
<kind>0</kind>
<type>8</type>
</field>
<field>
<name>IDS_LicenseProtected</name>
<value>1</value>
<visible>0</visible>
<kind>0</kind>
<type>5</type>
</field>
<field>
<name>IDS_LicenseUserName</name>
<value>someUserName</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_LicensePassword</name>
<value>somePassword</value>
<visible>-1</visible>
<kind>0</kind>
<type>1</type>
</field>
<field>
<name>IDS_LicensePurchaseDate</name>
<value>45789</value>
<visible>0</visible>
<kind>0</kind>
<type>3</type>
</field>
<field>
<name>IDS_LicenseOrderNumber</name>
<value>order number</value>
<visible>-1</visible>
<kind>0</kind>
<type>0</type>
</field>
<field>
<name>IDS_LicenseEmail</name>
<value>someEmail</value>
<visible>-1</visible>
<kind>0</kind>
<type>7</type>
</field>
<field>
<name>IDS_LicenseExpires</name>
<value>Nie</value>
<visible>0</visible>
<kind>0</kind>
<type>3</type>
</field>
</customfields>
</item>
</group>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo=
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg==
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,85 @@
export const TeamViewerTestData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/17.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>17.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
<group name="tempDB" fingerprint="CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D">
<item>
<description>TeamViewer type</description>
<type>10</type>
<password>somePassword</password>
<username></username>
<url>partnerId</url>
<comment>someNote</comment>
<lastmodified fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:08:14</lastmodified>
<expirydate fmt="dd.mm.yyyy">00.00.0000</expirydate>
<importance>1</importance>
<fingerprint>AE650032-5963-4D93-8E2E-69F216405C29</fingerprint>
<template></template>
<imageindex>124</imageindex>
<imagecustom>0</imagecustom>
<wndtitle></wndtitle>
<paramstr></paramstr>
<category>Allgemein</category>
<custombrowser></custombrowser>
<autocompletemethod>0</autocompletemethod>
<loginid></loginid>
<passid></passid>
<created fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:07:41</created>
<lastaccessed fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:07:41</lastaccessed>
<donotaddon>0</donotaddon>
<markassafe>0</markassafe>
<safemode>0</safemode>
<rndtype>1</rndtype>
<rndcustom></rndcustom>
<usagecount>0</usagecount>
<keephistory>2</keephistory>
<tags>someTag</tags>
<author>DJSMI</author>
<ownerid></ownerid>
<warnmsg></warnmsg>
<warnlvl>0</warnlvl>
<warnverify></warnverify>
<serverrqrd>0</serverrqrd>
<secret></secret>
<istemplate>0</istemplate>
<infotemplate></infotemplate>
<usetabs>161</usetabs>
<islink>0</islink>
<linkeditem></linkeditem>
<customfields>
<field>
<name>IDS_TeamViewerMode</name>
<value>0</value>
<visible>0</visible>
<kind>0</kind>
<type>0</type>
</field>
</customfields>
</item>
</group>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo=
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg==
</categories>
</passwordfile>`;

View File

@@ -0,0 +1,28 @@
export const InvalidVersionData = `<?xml version="1.0" encoding="utf-8"?>
<passwordfile xmlns="https://www.password-depot.de/schemas/passwordfile/18.0/passwordfile.xsd">
<header>
<application>Password Depot</application>
<version>18.0.0</version>
<comments></comments>
<hint></hint>
<fingerprint>CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D</fingerprint>
<datetime fmt="dd.mm.yyyy hh:mm:ss">12.05.2025 15:16:11</datetime>
<encrypted>False</encrypted>
<backupinterval>0</backupinterval>
<lastbackup fmt="dd.mm.yyyy hh:mm:ss">30.12.1899 00:00:00</lastbackup>
<hash></hash>
</header>
<passwords>
</passwords>
<recyclebin>
</recyclebin>
<infotemplates>
</infotemplates>
<favorites>
</favorites>
<sitestoignore>
</sitestoignore>
<categories>
</categories>
</passwordfile>`;

View File

@@ -73,6 +73,7 @@ export const regularImportOptions = [
{ id: "passkyjson", name: "Passky (json)" },
{ id: "passwordxpcsv", name: "Password XP (csv)" },
{ id: "netwrixpasswordsecure", name: "Netwrix Password Secure (csv)" },
{ id: "passworddepot17xml", name: "Password Depot 17 (xml)" },
] as const;
export type ImportType =

View File

@@ -90,6 +90,7 @@ import {
YotiCsvImporter,
ZohoVaultCsvImporter,
PasswordXPCsvImporter,
PasswordDepot17XmlImporter,
} from "../importers";
import { Importer } from "../importers/importer";
import {
@@ -348,6 +349,8 @@ export class ImportService implements ImportServiceAbstraction {
return new PasswordXPCsvImporter();
case "netwrixpasswordsecure":
return new NetwrixPasswordSecureCsvImporter();
case "passworddepot17xml":
return new PasswordDepot17XmlImporter();
default:
return null;
}

View File

@@ -0,0 +1,5 @@
# storage-test-utils
Owned by: platform
Test tools for the storage library

View File

@@ -0,0 +1,3 @@
import baseConfig from "../../eslint.config.mjs";
export default [...baseConfig];

View File

@@ -0,0 +1,10 @@
module.exports = {
displayName: "storage-test-utils",
preset: "../../jest.preset.js",
testEnvironment: "node",
transform: {
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
},
moduleFileExtensions: ["ts", "js", "html"],
coverageDirectory: "../../coverage/libs/storage-test-utils",
};

View File

@@ -0,0 +1,11 @@
{
"name": "@bitwarden/storage-test-utils",
"version": "0.0.1",
"description": "Test tools for the storage library",
"private": true,
"type": "commonjs",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "GPL-3.0",
"author": "platform"
}

View File

@@ -0,0 +1,33 @@
{
"name": "storage-test-utils",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/storage-test-utils/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/storage-test-utils",
"main": "libs/storage-test-utils/src/index.ts",
"tsConfig": "libs/storage-test-utils/tsconfig.lib.json",
"assets": ["libs/storage-test-utils/*.md"]
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/storage-test-utils/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/storage-test-utils/jest.config.js"
}
}
}
}

View File

@@ -0,0 +1,119 @@
import { MockProxy, mock } from "jest-mock-extended";
import { Subject } from "rxjs";
import {
AbstractStorageService,
ObservableStorageService,
StorageUpdate,
StorageOptions,
} from "@bitwarden/storage-core";
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;
}
}

View File

@@ -0,0 +1 @@
export * from "./fake-storage.service";

View File

@@ -0,0 +1,8 @@
import * as lib from "./index";
describe("storage-test-utils", () => {
// This test will fail until something is exported from index.ts
it("should work", () => {
expect(lib).toBeDefined();
});
});

Some files were not shown because too many files have changed in this diff Show More