1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 01:03:35 +00:00

Merge branch 'master' into EC-598-beeep-properly-store-passkeys-in-bitwarden

This commit is contained in:
Andreas Coroiu
2023-01-25 15:24:42 +01:00
1208 changed files with 94078 additions and 16018 deletions

View File

@@ -0,0 +1,20 @@
import { Observable } from "rxjs";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
import { FolderView } from "@bitwarden/common/models/view/folder.view";
import { DynamicTreeNode } from "../vault/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>;
checkForPersonalOwnershipPolicy: () => Promise<boolean>;
}

View File

@@ -1,7 +1,7 @@
import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
import { Directive, Input, OnChanges, SimpleChanges } from "@angular/core";
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { EventType } from "@bitwarden/common/enums/eventType";
@@ -25,7 +25,10 @@ export class AddEditCustomFieldsComponent implements OnChanges {
fieldType = FieldType;
eventType = EventType;
constructor(private i18nService: I18nService, private eventService: EventService) {
constructor(
private i18nService: I18nService,
private eventCollectionService: EventCollectionService
) {
this.addFieldTypeOptions = [
{ name: i18nService.t("cfTypeText"), value: FieldType.Text },
{ name: i18nService.t("cfTypeHidden"), value: FieldType.Hidden },
@@ -74,7 +77,10 @@ export class AddEditCustomFieldsComponent implements OnChanges {
const f = field as any;
f.showValue = !f.showValue;
if (this.editMode && f.showValue) {
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipher.id);
this.eventCollectionService.collect(
EventType.Cipher_ClientToggledHiddenFieldVisible,
this.cipher.id
);
}
}

View File

@@ -4,12 +4,15 @@ import { Observable, Subject, takeUntil, concatMap } from "rxjs";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import {
isNotProviderUser,
OrganizationService,
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
@@ -23,6 +26,7 @@ import { SecureNoteType } from "@bitwarden/common/enums/secureNoteType";
import { UriMatchType } from "@bitwarden/common/enums/uriMatchType";
import { Utils } from "@bitwarden/common/misc/utils";
import { Cipher } from "@bitwarden/common/models/domain/cipher";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { CardView } from "@bitwarden/common/models/view/card.view";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
@@ -74,7 +78,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
allowPersonal = true;
reprompt = false;
canUseReprompt = true;
organization: Organization;
protected componentName = "";
protected destroy$ = new Subject<void>();
protected writeableCollections: CollectionView[];
private personalOwnershipPolicyAppliesToActiveUser: boolean;
@@ -89,7 +95,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected stateService: StateService,
protected collectionService: CollectionService,
protected messagingService: MessagingService,
protected eventService: EventService,
protected eventCollectionService: EventCollectionService,
protected policyService: PolicyService,
private logService: LogService,
protected passwordRepromptService: PasswordRepromptService,
@@ -135,6 +141,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
{ name: i18nService.t("mr"), value: i18nService.t("mr") },
{ name: i18nService.t("mrs"), value: i18nService.t("mrs") },
{ name: i18nService.t("ms"), value: i18nService.t("ms") },
{ name: i18nService.t("mx"), value: i18nService.t("mx") },
{ name: i18nService.t("dr"), value: i18nService.t("dr") },
];
this.uriMatchOptions = [
@@ -183,11 +190,14 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
const orgs = await this.organizationService.getAll();
orgs.sort(Utils.getSortFunction(this.i18nService, "name")).forEach((o) => {
if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) {
this.ownershipOptions.push({ name: o.name, value: o.id });
}
});
orgs
.filter(isNotProviderUser)
.sort(Utils.getSortFunction(this.i18nService, "name"))
.forEach((o) => {
if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) {
this.ownershipOptions.push({ name: o.name, value: o.id });
}
});
if (!this.allowPersonal) {
this.organizationId = this.ownershipOptions[0].value;
}
@@ -264,7 +274,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.folders$ = this.folderService.folderViews$;
if (this.editMode && this.previousCipherId !== this.cipherId) {
this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId);
this.eventCollectionService.collect(EventType.Cipher_ClientViewed, this.cipherId);
}
this.previousCipherId = this.cipherId;
this.reprompt = this.cipher.reprompt !== CipherRepromptType.None;
@@ -395,7 +405,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.i18nService.t("deleteItem"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
"warning",
false,
this.componentName != "" ? this.componentName + " .modal-content" : null
);
if (!confirmed) {
return false;
@@ -487,14 +499,20 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.showPassword = !this.showPassword;
document.getElementById("loginPassword").focus();
if (this.editMode && this.showPassword) {
this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible, this.cipherId);
this.eventCollectionService.collect(
EventType.Cipher_ClientToggledPasswordVisible,
this.cipherId
);
}
}
async toggleCardNumber() {
this.showCardNumber = !this.showCardNumber;
if (this.showCardNumber) {
this.eventService.collect(EventType.Cipher_ClientToggledCardNumberVisible, this.cipherId);
this.eventCollectionService.collect(
EventType.Cipher_ClientToggledCardNumberVisible,
this.cipherId
);
}
}
@@ -502,7 +520,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.showCardCode = !this.showCardCode;
document.getElementById("cardCode").focus();
if (this.editMode && this.showCardCode) {
this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId);
this.eventCollectionService.collect(
EventType.Cipher_ClientToggledCardCodeVisible,
this.cipherId
);
}
}

View File

@@ -29,6 +29,7 @@ export class AttachmentsComponent implements OnInit {
deletePromises: { [id: string]: Promise<any> } = {};
reuploadPromises: { [id: string]: Promise<any> } = {};
emergencyAccessId?: string = null;
protected componentName = "";
constructor(
protected cipherService: CipherService,
@@ -104,7 +105,9 @@ export class AttachmentsComponent implements OnInit {
this.i18nService.t("deleteAttachment"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
"warning",
false,
this.componentName != "" ? this.componentName + " .modal-content" : null
);
if (!confirmed) {
return;
@@ -292,4 +295,8 @@ export class AttachmentsComponent implements OnInit {
protected deleteCipherAttachment(attachmentId: string) {
return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId);
}
protected async reupload(attachment: AttachmentView) {
// TODO: This should be removed but is needed since we re-use the same template
}
}

View File

@@ -3,7 +3,7 @@ import { UntypedFormBuilder, Validators } from "@angular/forms";
import { merge, takeUntil, Subject, startWith } from "rxjs";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { ExportService } from "@bitwarden/common/abstractions/export.service";
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
@@ -21,8 +21,6 @@ export class ExportComponent implements OnInit, OnDestroy {
formPromise: Promise<string>;
disabledByPolicy = false;
showFilePassword: boolean;
showConfirmFilePassword: boolean;
exportForm = this.formBuilder.group({
format: ["json"],
@@ -45,7 +43,7 @@ export class ExportComponent implements OnInit, OnDestroy {
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected exportService: ExportService,
protected eventService: EventService,
protected eventCollectionService: EventCollectionService,
private policyService: PolicyService,
protected win: Window,
private logService: LogService,
@@ -180,7 +178,7 @@ export class ExportComponent implements OnInit, OnDestroy {
}
protected async collectEvent(): Promise<void> {
await this.eventService.collect(EventType.User_ClientExportedVault);
await this.eventCollectionService.collect(EventType.User_ClientExportedVault);
}
get format() {
@@ -199,16 +197,6 @@ export class ExportComponent implements OnInit, OnDestroy {
return this.exportForm.get("fileEncryptionType").value;
}
toggleFilePassword() {
this.showFilePassword = !this.showFilePassword;
document.getElementById("filePassword").focus();
}
toggleConfirmFilePassword() {
this.showConfirmFilePassword = !this.showConfirmFilePassword;
document.getElementById("confirmFilePassword").focus();
}
adjustValidators() {
this.exportForm.get("confirmFilePassword").reset();
this.exportForm.get("filePassword").reset();

View File

@@ -18,6 +18,7 @@ export class FolderAddEditComponent implements OnInit {
title: string;
formPromise: Promise<any>;
deletePromise: Promise<any>;
protected componentName = "";
constructor(
protected folderService: FolderService,
@@ -65,7 +66,9 @@ export class FolderAddEditComponent implements OnInit {
this.i18nService.t("deleteFolder"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
"warning",
false,
this.componentName != "" ? this.componentName + " .modal-content" : null
);
if (!confirmed) {
return false;

View File

@@ -70,13 +70,7 @@ export class GeneratorComponent implements OnInit {
];
this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }];
this.forwardOptions = [
{ name: "SimpleLogin", value: "simplelogin" },
{ name: "AnonAddy", value: "anonaddy" },
{ name: "Firefox Relay", value: "firefoxrelay" },
{ name: "Fastmail", value: "fastmail" },
{ name: "DuckDuckGo", value: "duckduckgo" },
];
this.initForwardOptions();
}
async ngOnInit() {
@@ -239,4 +233,24 @@ export class GeneratorComponent implements OnInit {
this.enforcedPasswordPolicyOptions
);
}
private async initForwardOptions() {
this.forwardOptions = [
{ name: "AnonAddy", value: "anonaddy" },
{ name: "DuckDuckGo", value: "duckduckgo" },
{ name: "Fastmail", value: "fastmail" },
{ name: "Firefox Relay", value: "firefoxrelay" },
{ name: "SimpleLogin", value: "simplelogin" },
];
this.usernameOptions = await this.usernameGenerationService.getOptions();
if (
this.usernameOptions.forwardedService == null ||
this.usernameOptions.forwardedService === ""
) {
this.forwardOptions.push({ name: "", value: null });
}
this.forwardOptions = this.forwardOptions.sort((a, b) => a.name.localeCompare(b.name));
}
}

View File

@@ -7,5 +7,5 @@
decoding="async"
loading="lazy"
/>
<i class="bwi bwi-fw bwi-lg {{ icon }}" *ngIf="!imageEnabled || !image"></i>
<i class="tw-text-muted bwi bwi-lg {{ icon }}" *ngIf="!imageEnabled || !image"></i>
</div>

View File

@@ -46,8 +46,6 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
protected twoFactorRoute = "2fa";
protected successRoute = "vault";
protected forcePasswordResetRoute = "update-temp-password";
protected alwaysRememberEmail = false;
protected skipRememberEmail = false;
get loggedEmail() {
return this.formGroup.value.email;
@@ -85,6 +83,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
const queryParamsEmail = params["email"];
if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) {
this.formGroup.get("email").setValue(queryParamsEmail);
this.loginService.setEmail(queryParamsEmail);
this.paramEmailSet = true;
}
}
@@ -98,17 +97,11 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
if (!this.paramEmailSet) {
this.formGroup.get("email")?.setValue(email ?? "");
}
if (!this.alwaysRememberEmail) {
let rememberEmail = this.loginService.getRememberEmail();
if (rememberEmail == null) {
rememberEmail = (await this.stateService.getRememberedEmail()) != null;
}
this.formGroup.get("rememberEmail")?.setValue(rememberEmail);
}
if (email) {
this.validateEmail();
let rememberEmail = this.loginService.getRememberEmail();
if (rememberEmail == null) {
rememberEmail = (await this.stateService.getRememberedEmail()) != null;
}
this.formGroup.get("rememberEmail")?.setValue(rememberEmail);
}
async submit(showToast = true) {
@@ -140,11 +133,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
this.formPromise = this.authService.logIn(credentials);
const response = await this.formPromise;
this.setFormValues();
if (data.rememberEmail || this.alwaysRememberEmail) {
await this.stateService.setRememberedEmail(data.email);
} else {
await this.stateService.setRememberedEmail(null);
}
await this.loginService.saveEmailSettings();
if (this.handleCaptchaRequired(response)) {
return;
} else if (response.requiresTwoFactor) {
@@ -162,7 +151,6 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
} else {
const disableFavicon = await this.stateService.getDisableFavicon();
await this.stateService.setDisableFavicon(!!disableFavicon);
this.loginService.clearValues();
if (this.onSuccessfulLogin != null) {
this.onSuccessfulLogin();
}
@@ -189,6 +177,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
}
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
await this.saveEmailSettings();
// Generate necessary sso params
const passwordOptions: any = {
type: "password",
@@ -243,6 +232,11 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
this.loginService.setRememberEmail(this.formGroup.value.rememberEmail);
}
async saveEmailSettings() {
this.setFormValues();
await this.loginService.saveEmailSettings();
}
private getErrorToastMessage() {
const error: AllValidationErrors = this.formValidationErrorService
.getFormValidationErrors(this.formGroup.controls)

View File

@@ -3,6 +3,7 @@ import { AbstractControl, UntypedFormBuilder, ValidatorFn, Validators } from "@a
import { Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
@@ -38,6 +39,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
showTerms = true;
showErrorSummary = false;
passwordStrengthResult: any;
characterMinimumMessage: string;
minimumLength = 8;
color: string;
text: string;
@@ -45,8 +48,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
{
email: ["", [Validators.required, Validators.email]],
name: [""],
masterPassword: ["", [Validators.required, Validators.minLength(8)]],
confirmMasterPassword: ["", [Validators.required, Validators.minLength(8)]],
masterPassword: ["", [Validators.required, Validators.minLength(this.minimumLength)]],
confirmMasterPassword: ["", [Validators.required, Validators.minLength(this.minimumLength)]],
hint: [
null,
[
@@ -56,6 +59,7 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
),
],
],
checkForBreaches: [false],
acceptPolicies: [false, [this.acceptPoliciesValidation()]],
},
{
@@ -85,10 +89,12 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
platformUtilsService: PlatformUtilsService,
protected passwordGenerationService: PasswordGenerationService,
environmentService: EnvironmentService,
protected logService: LogService
protected logService: LogService,
protected auditService: AuditService
) {
super(environmentService, i18nService, platformUtilsService);
this.showTerms = !platformUtilsService.isSelfHost();
this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength);
}
async ngOnInit() {
@@ -212,7 +218,24 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
return { isValid: false };
}
if (this.passwordStrengthResult != null && this.passwordStrengthResult.score < 3) {
const passwordWeak =
this.passwordStrengthResult != null && this.passwordStrengthResult.score < 3;
const passwordLeak =
this.formGroup.controls.checkForBreaches.value &&
(await this.auditService.passwordLeaked(this.formGroup.controls.masterPassword.value)) > 0;
if (passwordWeak && passwordLeak) {
const result = await this.platformUtilsService.showDialog(
this.i18nService.t("weakAndBreachedMasterPasswordDesc"),
this.i18nService.t("weakAndExposedMasterPassword"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!result) {
return { isValid: false };
}
} else if (passwordWeak) {
const result = await this.platformUtilsService.showDialog(
this.i18nService.t("weakMasterPasswordDesc"),
this.i18nService.t("weakMasterPassword"),
@@ -223,7 +246,19 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
if (!result) {
return { isValid: false };
}
} else if (passwordLeak) {
const result = await this.platformUtilsService.showDialog(
this.i18nService.t("exposedMasterPasswordDesc"),
this.i18nService.t("exposedMasterPassword"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!result) {
return { isValid: false };
}
}
return { isValid: true };
}

View File

@@ -45,6 +45,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
alertShown = false;
showOptions = false;
protected componentName = "";
private sendLinkBaseUrl: string;
private destroy$ = new Subject<void>();
@@ -200,25 +201,14 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
this.onSavedSend.emit(this.send);
if (this.copyLink && this.link != null) {
const copySuccess = await this.copyLinkToClipboard(this.link);
if (copySuccess ?? true) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(this.editMode ? "editedSend" : "createdSend")
);
} else {
await this.platformUtilsService.showDialog(
this.i18nService.t(this.editMode ? "editedSend" : "createdSend"),
null,
this.i18nService.t("ok"),
null,
"success",
null
);
await this.copyLinkToClipboard(this.link);
}
await this.handleCopyLinkToClipboard();
return;
}
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(this.editMode ? "editedSend" : "createdSend")
);
});
try {
await this.formPromise;
@@ -242,7 +232,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.i18nService.t("deleteSend"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
"warning",
false,
this.componentName != "" ? this.componentName + " .modal-content" : null
);
if (!confirmed) {
return false;
@@ -305,4 +297,24 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.showPassword = !this.showPassword;
document.getElementById("password").focus();
}
private async handleCopyLinkToClipboard() {
const copySuccess = await this.copyLinkToClipboard(this.link);
if (copySuccess ?? true) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(this.editMode ? "editedSend" : "createdSend")
);
} else {
await this.platformUtilsService.showDialog(
this.i18nService.t(this.editMode ? "editedSend" : "createdSend"),
null,
this.i18nService.t("ok"),
null,
"success",
null
);
await this.copyLinkToClipboard(this.link);
}
}
}

View File

@@ -6,6 +6,8 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@@ -19,7 +21,6 @@ import { Utils } from "@bitwarden/common/misc/utils";
import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/models/request/organization-user-reset-password-enrollment.request";
import { SetPasswordRequest } from "@bitwarden/common/models/request/set-password.request";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
@@ -49,7 +50,8 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
private syncService: SyncService,
private route: ActivatedRoute,
stateService: StateService,
private organizationApiService: OrganizationApiServiceAbstraction
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService
) {
super(
i18nService,
@@ -136,7 +138,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
resetRequest.masterPasswordHash = masterPasswordHash;
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
return this.apiService.putOrganizationUserResetPasswordEnrollment(
return this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
this.orgId,
userId,
resetRequest

View File

@@ -5,7 +5,10 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import {
isNotProviderUser,
OrganizationService,
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
import { Utils } from "@bitwarden/common/misc/utils";
@@ -54,7 +57,10 @@ export class ShareComponent implements OnInit, OnDestroy {
this.organizations$ = this.organizationService.organizations$.pipe(
map((orgs) => {
return orgs
.filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed)
.filter(
(o) =>
o.enabled && o.status === OrganizationUserStatusType.Confirmed && isNotProviderUser(o)
)
.sort(Utils.getSortFunction(this.i18nService, "name"));
})
);

View File

@@ -1,46 +0,0 @@
<ng-container *ngIf="!usesKeyConnector">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="password"
name="MasterPasswordHash"
class="form-control"
[formControl]="secret"
required
appAutofocus
appInputVerbatim
/>
<small class="form-text text-muted">{{ "confirmIdentity" | i18n }}</small>
</ng-container>
<ng-container *ngIf="usesKeyConnector">
<div class="form-group">
<label class="d-block">{{ "sendVerificationCode" | i18n }}</label>
<button
type="button"
class="btn btn-outline-secondary"
(click)="requestOTP()"
[disabled]="disableRequestOTP"
>
{{ "sendCode" | i18n }}
</button>
<span class="ml-2 text-success" role="alert" @sent *ngIf="sentCode">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
{{ "codeSent" | i18n }}
</span>
</div>
<div class="form-group">
<label for="verificationCode">{{ "verificationCode" | i18n }}</label>
<input
id="verificationCode"
type="input"
name="verificationCode"
class="form-control"
[formControl]="secret"
required
appAutofocus
appInputVerbatim
/>
<small class="form-text text-muted">{{ "confirmIdentity" | i18n }}</small>
</div>
</ng-container>

View File

@@ -1,6 +1,5 @@
import { animate, style, transition, trigger } from "@angular/animations";
import { Component, OnInit } from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl } from "@angular/forms";
import { Directive, OnInit } from "@angular/core";
import { ControlValueAccessor, FormControl } from "@angular/forms";
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
@@ -14,21 +13,8 @@ import { Verification } from "@bitwarden/common/types/verification";
* This is exposed to the parent component via the ControlValueAccessor interface (e.g. bind it to a FormControl).
* Use UserVerificationService to verify the user's input.
*/
@Component({
@Directive({
selector: "app-user-verification",
templateUrl: "user-verification.component.html",
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: UserVerificationComponent,
},
],
animations: [
trigger("sent", [
transition(":enter", [style({ opacity: 0 }), animate("100ms", style({ opacity: 1 }))]),
]),
],
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class UserVerificationComponent implements ControlValueAccessor, OnInit {

View File

@@ -1,10 +1,11 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
@Directive()
export class CiphersComponent {
export class VaultItemsComponent {
@Input() activeCipherId: string = null;
@Output() onCipherClicked = new EventEmitter<CipherView>();
@Output() onCipherRightClicked = new EventEmitter<CipherView>();
@@ -13,19 +14,27 @@ export class CiphersComponent {
loaded = false;
ciphers: CipherView[] = [];
searchText: string;
searchPlaceholder: string = null;
filter: (cipher: CipherView) => boolean = null;
deleted = false;
organization: Organization;
accessEvents = false;
protected searchPending = false;
private searchTimeout: any = null;
private _searchText: string = null;
get searchText() {
return this._searchText;
}
set searchText(value: string) {
this._searchText = value;
}
constructor(protected searchService: SearchService) {}
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
this.deleted = deleted || false;
this.deleted = deleted ?? false;
await this.applyFilter(filter);
this.loaded = true;
}

View File

@@ -1,6 +1,6 @@
import { Directive, Input } from "@angular/core";
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventType } from "@bitwarden/common/enums/eventType";
import { FieldType } from "@bitwarden/common/enums/fieldType";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
@@ -14,7 +14,7 @@ export class ViewCustomFieldsComponent {
fieldType = FieldType;
constructor(private eventService: EventService) {}
constructor(private eventCollectionService: EventCollectionService) {}
async toggleFieldValue(field: FieldView) {
if (!(await this.promptPassword())) {
@@ -25,7 +25,10 @@ export class ViewCustomFieldsComponent {
f.showValue = !f.showValue;
f.showCount = false;
if (f.showValue) {
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipher.id);
this.eventCollectionService.collect(
EventType.Cipher_ClientToggledHiddenFieldVisible,
this.cipher.id
);
}
}

View File

@@ -15,7 +15,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
@@ -80,7 +80,7 @@ export class ViewComponent implements OnDestroy, OnInit {
protected broadcasterService: BroadcasterService,
protected ngZone: NgZone,
protected changeDetectorRef: ChangeDetectorRef,
protected eventService: EventService,
protected eventCollectionService: EventCollectionService,
protected apiService: ApiService,
protected passwordRepromptService: PasswordRepromptService,
private logService: LogService,
@@ -138,7 +138,7 @@ export class ViewComponent implements OnDestroy, OnInit {
}
if (this.previousCipherId !== this.cipherId) {
this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId);
this.eventCollectionService.collect(EventType.Cipher_ClientViewed, this.cipherId);
}
this.previousCipherId = this.cipherId;
}
@@ -238,7 +238,10 @@ export class ViewComponent implements OnDestroy, OnInit {
this.showPassword = !this.showPassword;
this.showPasswordCount = false;
if (this.showPassword) {
this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible, this.cipherId);
this.eventCollectionService.collect(
EventType.Cipher_ClientToggledPasswordVisible,
this.cipherId
);
}
}
@@ -257,7 +260,10 @@ export class ViewComponent implements OnDestroy, OnInit {
this.showCardNumber = !this.showCardNumber;
if (this.showCardNumber) {
this.eventService.collect(EventType.Cipher_ClientToggledCardNumberVisible, this.cipherId);
this.eventCollectionService.collect(
EventType.Cipher_ClientToggledCardNumberVisible,
this.cipherId
);
}
}
@@ -268,7 +274,10 @@ export class ViewComponent implements OnDestroy, OnInit {
this.showCardCode = !this.showCardCode;
if (this.showCardCode) {
this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId);
this.eventCollectionService.collect(
EventType.Cipher_ClientToggledCardCodeVisible,
this.cipherId
);
}
}
@@ -328,11 +337,14 @@ export class ViewComponent implements OnDestroy, OnInit {
);
if (typeI18nKey === "password") {
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipherId);
this.eventCollectionService.collect(
EventType.Cipher_ClientToggledHiddenFieldVisible,
this.cipherId
);
} else if (typeI18nKey === "securityCode") {
this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId);
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId);
} else if (aType === "H_Field") {
this.eventService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId);
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId);
}
}

View File

@@ -13,8 +13,8 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
],
})
export class TrueFalseValueDirective implements ControlValueAccessor {
@Input() trueValue = true;
@Input() falseValue = false;
@Input() trueValue: boolean | string = true;
@Input() falseValue: boolean | string = false;
constructor(private elementRef: ElementRef, private renderer: Renderer2) {}

View File

@@ -21,14 +21,13 @@ import { SelectCopyDirective } from "./directives/select-copy.directive";
import { StopClickDirective } from "./directives/stop-click.directive";
import { StopPropDirective } from "./directives/stop-prop.directive";
import { TrueFalseValueDirective } from "./directives/true-false-value.directive";
import { ColorPasswordCountPipe } from "./pipes/color-password-count.pipe";
import { ColorPasswordPipe } from "./pipes/color-password.pipe";
import { CreditCardNumberPipe } from "./pipes/credit-card-number.pipe";
import { EllipsisPipe } from "./pipes/ellipsis.pipe";
import { I18nPipe } from "./pipes/i18n.pipe";
import { SearchCiphersPipe } from "./pipes/search-ciphers.pipe";
import { SearchPipe } from "./pipes/search.pipe";
import { UserNamePipe } from "./pipes/user-name.pipe";
import { UserTypePipe } from "./pipes/user-type.pipe";
import { PasswordStrengthComponent } from "./shared/components/password-strength/password-strength.component";
@NgModule({
@@ -49,8 +48,6 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
AutofocusDirective,
BoxRowDirective,
CalloutComponent,
ColorPasswordCountPipe,
ColorPasswordPipe,
CreditCardNumberPipe,
EllipsisPipe,
ExportScopeCalloutComponent,
@@ -70,6 +67,7 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
LaunchClickDirective,
UserNamePipe,
PasswordStrengthComponent,
UserTypePipe,
],
exports: [
A11yInvalidDirective,
@@ -79,8 +77,6 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
BitwardenToastModule,
BoxRowDirective,
CalloutComponent,
ColorPasswordCountPipe,
ColorPasswordPipe,
CreditCardNumberPipe,
EllipsisPipe,
ExportScopeCalloutComponent,
@@ -100,7 +96,8 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
LaunchClickDirective,
UserNamePipe,
PasswordStrengthComponent,
UserTypePipe,
],
providers: [CreditCardNumberPipe, DatePipe, I18nPipe, SearchPipe, UserNamePipe],
providers: [CreditCardNumberPipe, DatePipe, I18nPipe, SearchPipe, UserNamePipe, UserTypePipe],
})
export class JslibModule {}

View File

@@ -8,7 +8,7 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
export class I18nPipe implements PipeTransform {
constructor(private i18nService: I18nService) {}
transform(id: string, p1?: string, p2?: string, p3?: string): string {
transform(id: string, p1?: string | number, p2?: string | number, p3?: string | number): string {
return this.i18nService.t(id, p1, p2, p3);
}
}

View File

@@ -1,16 +1,32 @@
import { Pipe, PipeTransform } from "@angular/core";
type PropertyValueFunction<T> = (item: T) => { toString: () => string };
@Pipe({
name: "search",
})
export class SearchPipe implements PipeTransform {
transform(
items: any[],
transform<T>(
items: T[],
searchText: string,
prop1?: string,
prop2?: string,
prop3?: string
): any[] {
prop1?: keyof T,
prop2?: keyof T,
prop3?: keyof T
): T[];
transform<T>(
items: T[],
searchText: string,
prop1?: PropertyValueFunction<T>,
prop2?: PropertyValueFunction<T>,
prop3?: PropertyValueFunction<T>
): T[];
transform<T>(
items: T[],
searchText: string,
prop1?: keyof T | PropertyValueFunction<T>,
prop2?: keyof T | PropertyValueFunction<T>,
prop3?: keyof T | PropertyValueFunction<T>
): T[] {
if (items == null || items.length === 0) {
return [];
}
@@ -21,27 +37,30 @@ export class SearchPipe implements PipeTransform {
searchText = searchText.trim().toLowerCase();
return items.filter((i) => {
if (
prop1 != null &&
i[prop1] != null &&
i[prop1].toString().toLowerCase().indexOf(searchText) > -1
) {
return true;
if (prop1 != null) {
const propValue = typeof prop1 === "function" ? prop1(i) : i[prop1];
if (propValue?.toString().toLowerCase().indexOf(searchText) > -1) {
return true;
}
}
if (
prop2 != null &&
i[prop2] != null &&
i[prop2].toString().toLowerCase().indexOf(searchText) > -1
) {
return true;
if (prop2 != null) {
const propValue = typeof prop2 === "function" ? prop2(i) : i[prop2];
if (propValue?.toString().toLowerCase().indexOf(searchText) > -1) {
return true;
}
}
if (
prop3 != null &&
i[prop3] != null &&
i[prop3].toString().toLowerCase().indexOf(searchText) > -1
) {
return true;
if (prop3 != null) {
const propValue = typeof prop3 === "function" ? prop3(i) : i[prop3];
if (propValue?.toString().toLowerCase().indexOf(searchText) > -1) {
return true;
}
}
return false;
});
}

View File

@@ -0,0 +1,29 @@
import { Pipe, PipeTransform } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
@Pipe({
name: "userType",
})
export class UserTypePipe implements PipeTransform {
constructor(private i18nService: I18nService) {}
transform(value?: OrganizationUserType): string {
if (value == null) {
return this.i18nService.t("unknown");
}
switch (value) {
case OrganizationUserType.Owner:
return this.i18nService.t("owner");
case OrganizationUserType.Admin:
return this.i18nService.t("admin");
case OrganizationUserType.User:
return this.i18nService.t("user");
case OrganizationUserType.Manager:
return this.i18nService.t("manager");
case OrganizationUserType.Custom:
return this.i18nService.t("custom");
}
}
}

View File

@@ -1,10 +1,13 @@
import { InjectionToken } from "@angular/core";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "@bitwarden/common/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
export const WINDOW = new InjectionToken<Window>("WINDOW");
export const MEMORY_STORAGE = new InjectionToken<AbstractStorageService>("MEMORY_STORAGE");
export const MEMORY_STORAGE = new InjectionToken<AbstractMemoryStorageService>("MEMORY_STORAGE");
export const SECURE_STORAGE = new InjectionToken<AbstractStorageService>("SECURE_STORAGE");
export const STATE_FACTORY = new InjectionToken<StateFactory>("STATE_FACTORY");
export const STATE_SERVICE_USE_CACHE = new InjectionToken<boolean>("STATE_SERVICE_USE_CACHE");

View File

@@ -2,9 +2,10 @@ import { Injector, LOCALE_ID, NgModule } from "@angular/core";
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/abstractions/account/account-api.service";
import {
InternalAccountService,
AccountService as AccountServiceAbstraction,
InternalAccountService,
} from "@bitwarden/common/abstractions/account/account.service";
import { AvatarUpdateService as AccountUpdateServiceAbstraction } from "@bitwarden/common/abstractions/account/avatar-update.service";
import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/abstractions/anonymousHub.service";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/abstractions/appId.service";
@@ -19,7 +20,8 @@ import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abs
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "@bitwarden/common/abstractions/environment.service";
import { EventService as EventServiceAbstraction } from "@bitwarden/common/abstractions/event.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 { ExportService as ExportServiceAbstraction } from "@bitwarden/common/abstractions/export.service";
import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/abstractions/fileUpload.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/abstractions/folder/folder-api.service.abstraction";
@@ -34,15 +36,19 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/abstractions/login.service";
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service";
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService as OrganizationServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import {
InternalOrganizationService,
OrganizationService as OrganizationServiceAbstraction,
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
import {
PolicyService as PolicyServiceAbstraction,
InternalPolicyService,
PolicyService as PolicyServiceAbstraction,
} from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/abstractions/provider.service";
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
@@ -68,6 +74,7 @@ import { Account } from "@bitwarden/common/models/domain/account";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { AccountApiServiceImplementation } from "@bitwarden/common/services/account/account-api.service";
import { AccountServiceImplementation } from "@bitwarden/common/services/account/account.service";
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { AnonymousHubService } from "@bitwarden/common/services/anonymousHub.service";
import { ApiService } from "@bitwarden/common/services/api.service";
import { AppIdService } from "@bitwarden/common/services/appId.service";
@@ -82,7 +89,8 @@ import { CryptoService } from "@bitwarden/common/services/crypto.service";
import { EncryptServiceImplementation } from "@bitwarden/common/services/cryptography/encrypt.service.implementation";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/services/cryptography/multithread-encrypt.service.implementation";
import { EnvironmentService } from "@bitwarden/common/services/environment.service";
import { EventService } from "@bitwarden/common/services/event.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
import { ExportService } from "@bitwarden/common/services/export.service";
import { FileUploadService } from "@bitwarden/common/services/fileUpload.service";
import { FolderApiService } from "@bitwarden/common/services/folder/folder-api.service";
@@ -91,6 +99,7 @@ import { FormValidationErrorsService } from "@bitwarden/common/services/formVali
import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service";
import { LoginService } from "@bitwarden/common/services/login.service";
import { NotificationsService } from "@bitwarden/common/services/notifications.service";
import { OrganizationUserServiceImplementation } from "@bitwarden/common/services/organization-user/organization-user.service.implementation";
import { OrganizationApiService } from "@bitwarden/common/services/organization/organization-api.service";
import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service";
@@ -121,16 +130,16 @@ import { UnauthGuard } from "../guards/unauth.guard";
import { BroadcasterService } from "./broadcaster.service";
import {
WINDOW,
LOCALES_DIRECTORY,
LOCKED_CALLBACK,
LOG_MAC_FAILURES,
LOGOUT_CALLBACK,
MEMORY_STORAGE,
SECURE_STORAGE,
STATE_FACTORY,
STATE_SERVICE_USE_CACHE,
LOGOUT_CALLBACK,
LOCKED_CALLBACK,
LOCALES_DIRECTORY,
SYSTEM_LANGUAGE,
LOG_MAC_FAILURES,
WINDOW,
} from "./injection-tokens";
import { ModalService } from "./modal.service";
import { PasswordRepromptService } from "./passwordReprompt.service";
@@ -284,6 +293,11 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
provide: InternalAccountService,
useExisting: AccountServiceAbstraction,
},
{
provide: AccountUpdateServiceAbstraction,
useClass: AvatarUpdateService,
deps: [ApiServiceAbstraction, StateServiceAbstraction],
},
{ provide: LogService, useFactory: () => new ConsoleLogService(false) },
{
provide: CollectionServiceAbstraction,
@@ -356,7 +370,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
StateServiceAbstraction,
ProviderServiceAbstraction,
FolderApiServiceAbstraction,
SyncNotifierServiceAbstraction,
OrganizationServiceAbstraction,
LOGOUT_CALLBACK,
],
},
@@ -454,14 +468,18 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
deps: [CryptoFunctionServiceAbstraction, LogService, LOG_MAC_FAILURES],
},
{
provide: EventServiceAbstraction,
useClass: EventService,
provide: EventUploadServiceAbstraction,
useClass: EventUploadService,
deps: [ApiServiceAbstraction, StateServiceAbstraction, LogService],
},
{
provide: EventCollectionServiceAbstraction,
useClass: EventCollectionService,
deps: [
ApiServiceAbstraction,
CipherServiceAbstraction,
StateServiceAbstraction,
LogService,
OrganizationServiceAbstraction,
EventUploadServiceAbstraction,
],
},
{
@@ -476,12 +494,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
{
provide: PolicyApiServiceAbstraction,
useClass: PolicyApiService,
deps: [
PolicyServiceAbstraction,
ApiServiceAbstraction,
StateServiceAbstraction,
OrganizationServiceAbstraction,
],
deps: [PolicyServiceAbstraction, ApiServiceAbstraction, StateServiceAbstraction],
},
{
provide: SendServiceAbstraction,
@@ -506,6 +519,8 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
LogService,
OrganizationServiceAbstraction,
CryptoFunctionServiceAbstraction,
SyncNotifierServiceAbstraction,
MessagingServiceAbstraction,
LOGOUT_CALLBACK,
],
},
@@ -522,7 +537,16 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
{
provide: OrganizationServiceAbstraction,
useClass: OrganizationService,
deps: [StateServiceAbstraction, SyncNotifierServiceAbstraction],
deps: [StateServiceAbstraction],
},
{
provide: InternalOrganizationService,
useExisting: OrganizationServiceAbstraction,
},
{
provide: OrganizationUserService,
useClass: OrganizationUserServiceImplementation,
deps: [ApiServiceAbstraction],
},
{
provide: ProviderServiceAbstraction,
@@ -583,6 +607,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
{
provide: LoginServiceAbstraction,
useClass: LoginService,
deps: [StateServiceAbstraction],
},
],
})

View File

@@ -6,10 +6,12 @@ import { ITreeNodeObject } from "@bitwarden/common/models/domain/tree-node";
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
import { FolderView } from "@bitwarden/common/models/view/folder.view";
import { DeprecatedVaultFilterService } from "../../../abstractions/deprecated-vault-filter.service";
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
import { VaultFilter } from "../models/vault-filter.model";
import { VaultFilterService } from "../services/vault-filter.service";
// TODO: Replace with refactored web vault filter component
// and refactor desktop/browser vault filters
@Directive()
export class VaultFilterComponent implements OnInit {
@Input() activeFilter: VaultFilter = new VaultFilter();
@@ -31,7 +33,7 @@ export class VaultFilterComponent implements OnInit {
collections: DynamicTreeNode<CollectionView>;
folders$: Observable<DynamicTreeNode<FolderView>>;
constructor(protected vaultFilterService: VaultFilterService) {}
constructor(protected vaultFilterService: DeprecatedVaultFilterService) {}
get displayCollections() {
return this.collections?.fullList != null && this.collections.fullList.length > 0;

View File

@@ -1,8 +1,6 @@
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
import { FolderView } from "@bitwarden/common/models/view/folder.view";
import { ITreeNodeObject, TreeNode } from "@bitwarden/common/models/domain/tree-node";
export class DynamicTreeNode<T extends CollectionView | FolderView> {
export class DynamicTreeNode<T extends ITreeNodeObject> {
fullList: T[];
nestedList: TreeNode<T>[];

View File

@@ -4,7 +4,10 @@ import { firstValueFrom, from, mergeMap, Observable } from "rxjs";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import {
isNotProviderUser,
OrganizationService,
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PolicyType } from "@bitwarden/common/enums/policyType";
@@ -14,12 +17,13 @@ import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
import { FolderView } from "@bitwarden/common/models/view/folder.view";
import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "../../../abstractions/deprecated-vault-filter.service";
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
const NestingDelimiter = "/";
@Injectable()
export class VaultFilterService {
export class VaultFilterService implements DeprecatedVaultFilterServiceAbstraction {
constructor(
protected stateService: StateService,
protected organizationService: OrganizationService,
@@ -40,7 +44,9 @@ export class VaultFilterService {
async buildOrganizations(): Promise<Organization[]> {
let organizations = await this.organizationService.getAll();
if (organizations != null) {
organizations = organizations.sort((a, b) => a.name.localeCompare(b.name));
organizations = organizations
.filter(isNotProviderUser)
.sort((a, b) => a.name.localeCompare(b.name));
}
return organizations;
@@ -115,6 +121,6 @@ export class VaultFilterService {
const folders = await this.getAllFoldersNested(
await firstValueFrom(this.folderService.folderViews$)
);
return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode<FolderView>;
return ServiceUtils.getTreeNodeObjectFromList(folders, id) as TreeNode<FolderView>;
}
}

View File

@@ -0,0 +1,3 @@
export async function awaitAsync(ms = 0) {
await new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -79,8 +79,8 @@ describe("Dashlane CSV Importer", () => {
expect(cipher2.card.cardholderName).toBe("John Doe");
expect(cipher2.card.number).toBe("41111111111111111");
expect(cipher2.card.code).toBe("123");
expect(cipher2.card.expMonth).toBe("01");
expect(cipher2.card.expYear).toBe("23");
expect(cipher2.card.expMonth).toBe("1");
expect(cipher2.card.expYear).toBe("2023");
expect(cipher2.fields.length).toBe(2);

View File

@@ -0,0 +1,133 @@
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { EnpassJsonImporter as Importer } from "@bitwarden/common/importers/enpass/enpass-json-importer";
import { FieldView } from "@bitwarden/common/models/view/field.view";
import { creditCard } from "./test-data/json/credit-card";
import { folders } from "./test-data/json/folders";
import { login } from "./test-data/json/login";
import { loginAndroidUrl } from "./test-data/json/login-android-url";
import { note } from "./test-data/json/note";
function validateCustomField(fields: FieldView[], fieldName: string, expectedValue: any) {
expect(fields).toBeDefined();
const customField = fields.find((f) => f.name === fieldName);
expect(customField).toBeDefined();
expect(customField.value).toEqual(expectedValue);
}
describe("Enpass JSON Importer", () => {
it("should create folders/ nested folder and assignment", async () => {
const importer = new Importer();
const testDataString = JSON.stringify(folders);
const result = await importer.parse(testDataString);
expect(result != null).toBe(true);
expect(result.folders.length).toEqual(2);
const folder1 = result.folders.shift();
expect(folder1.name).toEqual("Social");
// Created 2 folders and Twitter is child of Social
const folder2 = result.folders.shift();
expect(folder2.name).toEqual("Social/Twitter");
// Expect that the single cipher item is assigned to sub-folder "Social/Twitter"
const folderRelationship = result.folderRelationships.shift();
expect(folderRelationship).toEqual([0, 1]);
});
it("should parse login items", async () => {
const importer = new Importer();
const testDataString = JSON.stringify(login);
const result = await importer.parse(testDataString);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.Login);
expect(cipher.name).toEqual("Amazon");
expect(cipher.subTitle).toEqual("emily@enpass.io");
expect(cipher.favorite).toBe(true);
expect(cipher.notes).toEqual("some notes on the login item");
expect(cipher.login.username).toEqual("emily@enpass.io");
expect(cipher.login.password).toEqual("$&W:v@}4\\iRpUXVbjPdPKDGbD<xK>");
expect(cipher.login.totp).toEqual("TOTP_SEED_VALUE");
expect(cipher.login.uris.length).toEqual(1);
const uriView = cipher.login.uris.shift();
expect(uriView.uri).toEqual("https://www.amazon.com");
// remaining fields as custom fields
expect(cipher.fields.length).toEqual(3);
validateCustomField(cipher.fields, "Phone number", "12345678");
validateCustomField(cipher.fields, "Security question", "SECURITY_QUESTION");
validateCustomField(cipher.fields, "Security answer", "SECURITY_ANSWER");
});
it("should parse login items with Android Autofill information", async () => {
const importer = new Importer();
const testDataString = JSON.stringify(loginAndroidUrl);
const result = await importer.parse(testDataString);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.Login);
expect(cipher.name).toEqual("Amazon");
expect(cipher.login.uris.length).toEqual(5);
expect(cipher.login.uris[0].uri).toEqual("https://www.amazon.com");
expect(cipher.login.uris[1].uri).toEqual("androidapp://com.amazon.0");
expect(cipher.login.uris[2].uri).toEqual("androidapp://com.amazon.1");
expect(cipher.login.uris[3].uri).toEqual("androidapp://com.amazon.2");
expect(cipher.login.uris[4].uri).toEqual("androidapp://com.amazon.3");
});
it("should parse credit card items", async () => {
const importer = new Importer();
const testDataString = JSON.stringify(creditCard);
const result = await importer.parse(testDataString);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.Card);
expect(cipher.name).toEqual("Emily Sample Credit Card");
expect(cipher.subTitle).toEqual("Amex, *10005");
expect(cipher.favorite).toBe(true);
expect(cipher.notes).toEqual("some notes on the credit card");
expect(cipher.card.cardholderName).toEqual("Emily Sample");
expect(cipher.card.number).toEqual("3782 822463 10005");
expect(cipher.card.brand).toEqual("Amex");
expect(cipher.card.code).toEqual("1234");
expect(cipher.card.expMonth).toEqual("3");
expect(cipher.card.expYear).toEqual("23");
// remaining fields as custom fields
expect(cipher.fields.length).toEqual(9);
validateCustomField(cipher.fields, "PIN", "9874");
validateCustomField(cipher.fields, "Username", "Emily_ENP");
validateCustomField(
cipher.fields,
"Login password",
"nnn tug shoot selfish bon liars convent dusty minnow uncheck"
);
validateCustomField(cipher.fields, "Website", "http://global.americanexpress.com/");
validateCustomField(cipher.fields, "Issuing bank", "American Express");
validateCustomField(cipher.fields, "Credit limit", "100000");
validateCustomField(cipher.fields, "Withdrawal limit", "50000");
validateCustomField(cipher.fields, "Interest rate", "1.5");
validateCustomField(cipher.fields, "If lost, call", "12345678");
});
it("should parse notes", async () => {
const importer = new Importer();
const testDataString = JSON.stringify(note);
const result = await importer.parse(testDataString);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.SecureNote);
expect(cipher.name).toEqual("some secure note title");
expect(cipher.favorite).toBe(false);
expect(cipher.notes).toEqual("some secure note content");
});
});

View File

@@ -0,0 +1,274 @@
import { EnpassJsonFile } from "@bitwarden/common/importers/enpass/types/enpass-json-type";
export const creditCard: EnpassJsonFile = {
folders: [],
items: [
{
archived: 0,
auto_submit: 1,
category: "creditcard",
createdAt: 1666449561,
favorite: 1,
fields: [
{
deleted: 0,
history: [
{
updated_at: 1534490234,
value: "Wendy Apple Seed",
},
{
updated_at: 1535521811,
value: "Emma",
},
{
updated_at: 1535522090,
value: "Emily",
},
],
label: "Cardholder",
order: 1,
sensitive: 0,
type: "ccName",
uid: 0,
updated_at: 1666449561,
value: "Emily Sample",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Type",
order: 2,
sensitive: 0,
type: "ccType",
uid: 17,
updated_at: 1666449561,
value: "American Express",
value_updated_at: 1666449561,
},
{
deleted: 0,
history: [
{
updated_at: 1534490234,
value: "1234 1234 5678 0000",
},
],
label: "Number",
order: 3,
sensitive: 0,
type: "ccNumber",
uid: 1,
updated_at: 1666449561,
value: "3782 822463 10005",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "CVC",
order: 4,
sensitive: 1,
type: "ccCvc",
uid: 2,
updated_at: 1666449561,
value: "1234",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "PIN",
order: 5,
sensitive: 1,
type: "ccPin",
uid: 3,
updated_at: 1666449561,
value: "9874",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Expiry date",
order: 6,
sensitive: 0,
type: "ccExpiry",
uid: 4,
updated_at: 1666449561,
value: "03/23",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "INTERNET BANKING",
order: 7,
sensitive: 0,
type: "section",
uid: 103,
updated_at: 1666449561,
value: "",
value_updated_at: 1666449561,
},
{
deleted: 0,
history: [
{
updated_at: 1534490234,
value: "WendySeed",
},
{
updated_at: 1535521811,
value: "Emma1",
},
{
updated_at: 1535522182,
value: "Emily1",
},
],
label: "Username",
order: 8,
sensitive: 0,
type: "username",
uid: 15,
updated_at: 1666449561,
value: "Emily_ENP",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Login password",
order: 9,
sensitive: 1,
type: "password",
uid: 16,
updated_at: 1666449561,
value: "nnn tug shoot selfish bon liars convent dusty minnow uncheck",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Transaction password",
order: 10,
sensitive: 1,
type: "ccTxnpassword",
uid: 9,
updated_at: 1666449561,
value: "",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Website",
order: 11,
sensitive: 0,
type: "url",
uid: 14,
updated_at: 1666449561,
value: "http://global.americanexpress.com/",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "ADDITIONAL DETAILS",
order: 12,
sensitive: 0,
type: "section",
uid: 104,
updated_at: 1666449561,
value: "",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Issuing bank",
order: 13,
sensitive: 0,
type: "ccBankname",
uid: 6,
updated_at: 1666449561,
value: "American Express",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Issued on",
order: 14,
sensitive: 0,
type: "date",
uid: 7,
updated_at: 1666449561,
value: "",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Valid from",
order: 15,
sensitive: 0,
type: "ccValidfrom",
uid: 18,
updated_at: 1666449561,
value: "",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Credit limit",
order: 16,
sensitive: 0,
type: "numeric",
uid: 10,
updated_at: 1666449561,
value: "100000",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Withdrawal limit",
order: 17,
sensitive: 0,
type: "numeric",
uid: 11,
updated_at: 1666449561,
value: "50000",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Interest rate",
order: 18,
sensitive: 0,
type: "numeric",
uid: 12,
updated_at: 1666449561,
value: "1.5",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "If lost, call",
order: 19,
sensitive: 0,
type: "phone",
uid: 8,
updated_at: 1666449561,
value: "12345678",
value_updated_at: 1666449561,
},
],
icon: {
fav: "global.americanexpress.com",
image: {
file: "cc/american_express",
},
type: 2,
uuid: "",
},
note: "some notes on the credit card",
subtitle: "***** 0000",
template_type: "creditcard.default",
title: "Emily Sample Credit Card",
trashed: 0,
updated_at: 1666554351,
uuid: "dbbc741b-81d6-491a-9660-92995fd8958c",
},
],
};

View File

@@ -0,0 +1,45 @@
import { EnpassJsonFile } from "@bitwarden/common/importers/enpass/types/enpass-json-type";
export const folders: EnpassJsonFile = {
folders: [
{
icon: "1008",
parent_uuid: "",
title: "Social",
updated_at: 1666449561,
uuid: "7b2ed0da-8cd9-445f-9a1a-490ca2b9ffbc",
},
{
icon: "1008",
parent_uuid: "7b2ed0da-8cd9-445f-9a1a-490ca2b9ffbc",
title: "Twitter",
updated_at: 1666450857,
uuid: "7fe8a8bc-b848-4f9f-9870-c2936317e74d",
},
],
items: [
{
archived: 0,
auto_submit: 1,
category: "note",
createdAt: 1666554621,
favorite: 0,
folders: ["7fe8a8bc-b848-4f9f-9870-c2936317e74d"],
icon: {
fav: "",
image: {
file: "misc/secure_note",
},
type: 1,
uuid: "",
},
note: "some secure note content",
subtitle: "",
template_type: "note.default",
title: "some secure note title",
trashed: 0,
updated_at: 1666554621,
uuid: "8b5ea2f6-f62b-4fec-a235-4a40946026b6",
},
],
};

View File

@@ -0,0 +1,79 @@
import { EnpassJsonFile } from "@bitwarden/common/importers/enpass/types/enpass-json-type";
import { login } from "./login";
export const loginAndroidUrl: EnpassJsonFile = {
folders: [],
items: [
{
archived: 0,
auto_submit: 1,
category: "login",
createdAt: 1666449561,
favorite: 1,
fields: [
...login.items[0].fields,
{
deleted: 0,
label: "Autofill Info",
order: 9,
sensitive: 0,
type: ".Android#",
uid: 7696,
updated_at: 1666551057,
value: "com.amazon.0",
value_updated_at: 1666551057,
},
{
deleted: 0,
label: "Autofill Info 1",
order: 9,
sensitive: 0,
type: ".Android#",
uid: 7696,
updated_at: 1666551057,
value:
"android://pMUhLBalOhcc3yK-84sMiGc2U856FVVUhm8PZveoRfNFT3ocT1KWZlciAkF2ED--B5i_fMuNlC6JfPxcHk1AQg==@com.amazon.1",
value_updated_at: 1666551057,
},
{
deleted: 0,
label: "Autofill Info2 ",
order: 9,
sensitive: 0,
type: ".Android#",
uid: 7696,
updated_at: 1666551057,
value: "android://com.amazon.2",
value_updated_at: 1666551057,
},
{
deleted: 0,
label: "Autofill Info 3",
order: 9,
sensitive: 0,
type: ".Android#",
uid: 7696,
updated_at: 1666551057,
value: "androidapp://com.amazon.3",
value_updated_at: 1666551057,
},
],
icon: {
fav: "www.amazon.com",
image: {
file: "web/amazon.com",
},
type: 1,
uuid: "",
},
note: "some notes on the login item",
subtitle: "emily@enpass.io",
template_type: "login.default",
title: "Amazon",
trashed: 0,
updated_at: 1666449561,
uuid: "f717cb7c-6cce-4b24-b023-ec8a429cc992",
},
],
};

View File

@@ -0,0 +1,130 @@
import { EnpassJsonFile } from "@bitwarden/common/importers/enpass/types/enpass-json-type";
export const login: EnpassJsonFile = {
folders: [],
items: [
{
archived: 0,
auto_submit: 1,
category: "login",
createdAt: 1666449561,
favorite: 1,
fields: [
{
deleted: 0,
label: "Username",
order: 1,
sensitive: 0,
type: "username",
uid: 10,
updated_at: 1666449561,
value: "",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "E-mail",
order: 2,
sensitive: 0,
type: "email",
uid: 12,
updated_at: 1666449561,
value: "emily@enpass.io",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Password",
order: 3,
sensitive: 1,
type: "password",
uid: 11,
updated_at: 1666449561,
value: "$&W:v@}4\\iRpUXVbjPdPKDGbD<xK>",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Website",
order: 4,
sensitive: 0,
type: "url",
uid: 13,
updated_at: 1666449561,
value: "https://www.amazon.com",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "ADDITIONAL DETAILS",
order: 5,
sensitive: 0,
type: "section",
uid: 101,
updated_at: 1666449561,
value: "",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Phone number",
order: 6,
sensitive: 0,
type: "phone",
uid: 14,
updated_at: 1666449561,
value: "12345678",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "TOTP",
order: 7,
sensitive: 0,
type: "totp",
uid: 102,
updated_at: 1666449561,
value: "TOTP_SEED_VALUE",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Security question",
order: 8,
sensitive: 0,
type: "text",
uid: 15,
updated_at: 1666449561,
value: "SECURITY_QUESTION",
value_updated_at: 1666449561,
},
{
deleted: 0,
label: "Security answer",
order: 9,
sensitive: 1,
type: "text",
uid: 16,
updated_at: 1666449561,
value: "SECURITY_ANSWER",
value_updated_at: 1666449561,
},
],
icon: {
fav: "www.amazon.com",
image: {
file: "web/amazon.com",
},
type: 1,
uuid: "",
},
note: "some notes on the login item",
subtitle: "emily@enpass.io",
template_type: "login.default",
title: "Amazon",
trashed: 0,
updated_at: 1666449561,
uuid: "f717cb7c-6cce-4b24-b023-ec8a429cc992",
},
],
};

View File

@@ -0,0 +1,29 @@
import { EnpassJsonFile } from "@bitwarden/common/importers/enpass/types/enpass-json-type";
export const note: EnpassJsonFile = {
folders: [],
items: [
{
archived: 0,
auto_submit: 1,
category: "note",
createdAt: 1666554621,
favorite: 0,
icon: {
fav: "",
image: {
file: "misc/secure_note",
},
type: 1,
uuid: "",
},
note: "some secure note content",
subtitle: "",
template_type: "note.default",
title: "some secure note title",
trashed: 0,
updated_at: 1666554621,
uuid: "8b5ea2f6-f62b-4fec-a235-4a40946026b6",
},
],
};

View File

@@ -1,77 +0,0 @@
import { FSecureFskImporter as Importer } from "@bitwarden/common/importers/fsecure-fsk-importer";
const TestDataWithStyleSetToWebsite: string = JSON.stringify({
data: {
"8d58b5cf252dd06fbd98f5289e918ab1": {
color: "#00baff",
reatedDate: 1609302913,
creditCvv: "",
creditExpiry: "",
creditNumber: "",
favorite: 0,
modifiedDate: 1609302913,
notes: "note",
password: "word",
passwordList: [],
passwordModifiedDate: 1609302913,
rev: 1,
service: "My first pass",
style: "website",
type: 1,
url: "https://bitwarden.com",
username: "pass",
},
},
});
const TestDataWithStyleSetToGlobe: string = JSON.stringify({
data: {
"8d58b5cf252dd06fbd98f5289e918ab1": {
color: "#00baff",
reatedDate: 1609302913,
creditCvv: "",
creditExpiry: "",
creditNumber: "",
favorite: 0,
modifiedDate: 1609302913,
notes: "note",
password: "word",
passwordList: [],
passwordModifiedDate: 1609302913,
rev: 1,
service: "My first pass",
style: "globe",
type: 1,
url: "https://bitwarden.com",
username: "pass",
},
},
});
describe("FSecure FSK Importer", () => {
it("should parse data with style set to website", async () => {
const importer = new Importer();
const result = await importer.parse(TestDataWithStyleSetToWebsite);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.login.username).toEqual("pass");
expect(cipher.login.password).toEqual("word");
expect(cipher.login.uris.length).toEqual(1);
const uriView = cipher.login.uris.shift();
expect(uriView.uri).toEqual("https://bitwarden.com");
});
it("should parse data with style set to globe", async () => {
const importer = new Importer();
const result = await importer.parse(TestDataWithStyleSetToGlobe);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.login.username).toEqual("pass");
expect(cipher.login.password).toEqual("word");
expect(cipher.login.uris.length).toEqual(1);
const uriView = cipher.login.uris.shift();
expect(uriView.uri).toEqual("https://bitwarden.com");
});
});

View File

@@ -0,0 +1,533 @@
export const TestData = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<KeePassFile>
<Meta>
<Generator>KeePass</Generator>
<DatabaseName />
<DatabaseNameChanged>2016-12-31T21:33:52Z</DatabaseNameChanged>
<DatabaseDescription />
<DatabaseDescriptionChanged>2016-12-31T21:33:52Z</DatabaseDescriptionChanged>
<DefaultUserName />
<DefaultUserNameChanged>2016-12-31T21:33:52Z</DefaultUserNameChanged>
<MaintenanceHistoryDays>365</MaintenanceHistoryDays>
<Color />
<MasterKeyChanged>2016-12-31T21:33:59Z</MasterKeyChanged>
<MasterKeyChangeRec>-1</MasterKeyChangeRec>
<MasterKeyChangeForce>-1</MasterKeyChangeForce>
<MemoryProtection>
<ProtectTitle>False</ProtectTitle>
<ProtectUserName>False</ProtectUserName>
<ProtectPassword>True</ProtectPassword>
<ProtectURL>False</ProtectURL>
<ProtectNotes>False</ProtectNotes>
</MemoryProtection>
<RecycleBinEnabled>True</RecycleBinEnabled>
<RecycleBinUUID>AAAAAAAAAAAAAAAAAAAAAA==</RecycleBinUUID>
<RecycleBinChanged>2016-12-31T21:33:52Z</RecycleBinChanged>
<EntryTemplatesGroup>AAAAAAAAAAAAAAAAAAAAAA==</EntryTemplatesGroup>
<EntryTemplatesGroupChanged>2016-12-31T21:33:52Z</EntryTemplatesGroupChanged>
<HistoryMaxItems>10</HistoryMaxItems>
<HistoryMaxSize>6291456</HistoryMaxSize>
<LastSelectedGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastSelectedGroup>
<LastTopVisibleGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleGroup>
<Binaries />
<CustomData />
</Meta>
<Root>
<Group>
<UUID>KvS57lVwl13AfGFLwkvq4Q==</UUID>
<Name>Root</Name>
<Notes />
<IconID>48</IconID>
<Times>
<CreationTime>2016-12-31T21:33:52Z</CreationTime>
<LastModificationTime>2016-12-31T21:33:52Z</LastModificationTime>
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:33:52Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>1</UsageCount>
<LocationChanged>2016-12-31T21:33:52Z</LocationChanged>
</Times>
<IsExpanded>True</IsExpanded>
<DefaultAutoTypeSequence />
<EnableAutoType>null</EnableAutoType>
<EnableSearching>null</EnableSearching>
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
<Group>
<UUID>P0ParXgGMBW6caOL2YrhqQ==</UUID>
<Name>Folder2</Name>
<Notes>a note about the folder</Notes>
<IconID>48</IconID>
<Times>
<CreationTime>2016-12-31T21:43:30Z</CreationTime>
<LastModificationTime>2016-12-31T21:43:43Z</LastModificationTime>
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:43:30Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>1</UsageCount>
<LocationChanged>2016-12-31T21:43:43Z</LocationChanged>
</Times>
<IsExpanded>True</IsExpanded>
<DefaultAutoTypeSequence />
<EnableAutoType>null</EnableAutoType>
<EnableSearching>null</EnableSearching>
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
<Entry>
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
<IconID>1</IconID>
<ForegroundColor />
<BackgroundColor />
<OverrideURL />
<Tags />
<Times>
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
<LastModificationTime>2016-12-31T21:40:23Z</LastModificationTime>
<LastAccessTime>2016-12-31T21:40:23Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>2016-12-31T21:43:48Z</LocationChanged>
</Times>
<String>
<Key>att2</Key>
<Value>att2value</Value>
</String>
<String>
<Key>attr1</Key>
<Value>att1value
line1
line2</Value>
</String>
<String>
<Key>Notes</Key>
<Value>This is a note!!!
line1
line2</Value>
</String>
<String>
<Key>Password</Key>
<Value ProtectInMemory="True">googpass</Value>
</String>
<String>
<Key>Title</Key>
<Value>Google</Value>
</String>
<String>
<Key>URL</Key>
<Value>google.com</Value>
</String>
<String>
<Key>UserName</Key>
<Value>googleuser</Value>
</String>
<AutoType>
<Enabled>True</Enabled>
<DataTransferObfuscation>0</DataTransferObfuscation>
</AutoType>
<History>
<Entry>
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
<IconID>0</IconID>
<ForegroundColor />
<BackgroundColor />
<OverrideURL />
<Tags />
<Times>
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
<LastModificationTime>2016-12-31T21:34:40Z</LastModificationTime>
<LastAccessTime>2016-12-31T21:34:40Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>2016-12-31T21:34:40Z</LocationChanged>
</Times>
<String>
<Key>Notes</Key>
<Value>This is a note!!!
line1
line2</Value>
</String>
<String>
<Key>Password</Key>
<Value ProtectInMemory="True">googpass</Value>
</String>
<String>
<Key>Title</Key>
<Value>Google</Value>
</String>
<String>
<Key>URL</Key>
<Value>google.com</Value>
</String>
<String>
<Key>UserName</Key>
<Value>googleuser</Value>
</String>
<AutoType>
<Enabled>True</Enabled>
<DataTransferObfuscation>0</DataTransferObfuscation>
</AutoType>
</Entry>
</History>
</Entry>
</Group>
</Group>
<DeletedObjects />
</Root>
</KeePassFile>`;
export const TestData1 = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<KeePassFile>
<Meta>
<Generator>KeePass</Generator>
<DatabaseName />
<DatabaseNameChanged>2016-12-31T21:33:52Z</DatabaseNameChanged>
<DatabaseDescription />
<DatabaseDescriptionChanged>2016-12-31T21:33:52Z</DatabaseDescriptionChanged>
<DefaultUserName />
<DefaultUserNameChanged>2016-12-31T21:33:52Z</DefaultUserNameChanged>
<MaintenanceHistoryDays>365</MaintenanceHistoryDays>
<Color />
<MasterKeyChanged>2016-12-31T21:33:59Z</MasterKeyChanged>
<MasterKeyChangeRec>-1</MasterKeyChangeRec>
<MasterKeyChangeForce>-1</MasterKeyChangeForce>
<MemoryProtection>
<ProtectTitle>False</ProtectTitle>
<ProtectUserName>False</ProtectUserName>
<ProtectPassword>True</ProtectPassword>
<ProtectURL>False</ProtectURL>
<ProtectNotes>False</ProtectNotes>
</MemoryProtection>
<RecycleBinEnabled>True</RecycleBinEnabled>
<RecycleBinUUID>AAAAAAAAAAAAAAAAAAAAAA==</RecycleBinUUID>
<RecycleBinChanged>2016-12-31T21:33:52Z</RecycleBinChanged>
<EntryTemplatesGroup>AAAAAAAAAAAAAAAAAAAAAA==</EntryTemplatesGroup>
<EntryTemplatesGroupChanged>2016-12-31T21:33:52Z</EntryTemplatesGroupChanged>
<HistoryMaxItems>10</HistoryMaxItems>
<HistoryMaxSize>6291456</HistoryMaxSize>
<LastSelectedGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastSelectedGroup>
<LastTopVisibleGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleGroup>
<Binaries />
<CustomData />
</Meta>
<Group>
<UUID>KvS57lVwl13AfGFLwkvq4Q==</UUID>
<Name>Root</Name>
<Notes />
<IconID>48</IconID>
<Times>
<CreationTime>2016-12-31T21:33:52Z</CreationTime>
<LastModificationTime>2016-12-31T21:33:52Z</LastModificationTime>
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:33:52Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>1</UsageCount>
<LocationChanged>2016-12-31T21:33:52Z</LocationChanged>
</Times>
<IsExpanded>True</IsExpanded>
<DefaultAutoTypeSequence />
<EnableAutoType>null</EnableAutoType>
<EnableSearching>null</EnableSearching>
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
<Group>
<UUID>P0ParXgGMBW6caOL2YrhqQ==</UUID>
<Name>Folder2</Name>
<Notes>a note about the folder</Notes>
<IconID>48</IconID>
<Times>
<CreationTime>2016-12-31T21:43:30Z</CreationTime>
<LastModificationTime>2016-12-31T21:43:43Z</LastModificationTime>
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:43:30Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>1</UsageCount>
<LocationChanged>2016-12-31T21:43:43Z</LocationChanged>
</Times>
<IsExpanded>True</IsExpanded>
<DefaultAutoTypeSequence />
<EnableAutoType>null</EnableAutoType>
<EnableSearching>null</EnableSearching>
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
<Entry>
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
<IconID>1</IconID>
<ForegroundColor />
<BackgroundColor />
<OverrideURL />
<Tags />
<Times>
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
<LastModificationTime>2016-12-31T21:40:23Z</LastModificationTime>
<LastAccessTime>2016-12-31T21:40:23Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>2016-12-31T21:43:48Z</LocationChanged>
</Times>
<String>
<Key>att2</Key>
<Value>att2value</Value>
</String>
<String>
<Key>attr1</Key>
<Value>att1value
line1
line2</Value>
</String>
<String>
<Key>Notes</Key>
<Value>This is a note!!!
line1
line2</Value>
</String>
<String>
<Key>Password</Key>
<Value ProtectInMemory="True">googpass</Value>
</String>
<String>
<Key>Title</Key>
<Value>Google</Value>
</String>
<String>
<Key>URL</Key>
<Value>google.com</Value>
</String>
<String>
<Key>UserName</Key>
<Value>googleuser</Value>
</String>
<AutoType>
<Enabled>True</Enabled>
<DataTransferObfuscation>0</DataTransferObfuscation>
</AutoType>
<History>
<Entry>
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
<IconID>0</IconID>
<ForegroundColor />
<BackgroundColor />
<OverrideURL />
<Tags />
<Times>
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
<LastModificationTime>2016-12-31T21:34:40Z</LastModificationTime>
<LastAccessTime>2016-12-31T21:34:40Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>2016-12-31T21:34:40Z</LocationChanged>
</Times>
<String>
<Key>Notes</Key>
<Value>This is a note!!!
line1
line2</Value>
</String>
<String>
<Key>Password</Key>
<Value ProtectInMemory="True">googpass</Value>
</String>
<String>
<Key>Title</Key>
<Value>Google</Value>
</String>
<String>
<Key>URL</Key>
<Value>google.com</Value>
</String>
<String>
<Key>UserName</Key>
<Value>googleuser</Value>
</String>
<AutoType>
<Enabled>True</Enabled>
<DataTransferObfuscation>0</DataTransferObfuscation>
</AutoType>
</Entry>
</History>
</Entry>
</Group>
</Group>
<DeletedObjects />
</KeePassFile>`;
export const TestData2 = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Meta>
<Generator>KeePass</Generator>
<DatabaseName />
<DatabaseNameChanged>2016-12-31T21:33:52Z</DatabaseNameChanged>
<DatabaseDescription />
<DatabaseDescriptionChanged>2016-12-31T21:33:52Z</DatabaseDescriptionChanged>
<DefaultUserName />
<DefaultUserNameChanged>2016-12-31T21:33:52Z</DefaultUserNameChanged>
<MaintenanceHistoryDays>365</MaintenanceHistoryDays>
<Color />
<MasterKeyChanged>2016-12-31T21:33:59Z</MasterKeyChanged>
<MasterKeyChangeRec>-1</MasterKeyChangeRec>
<MasterKeyChangeForce>-1</MasterKeyChangeForce>
<MemoryProtection>
<ProtectTitle>False</ProtectTitle>
<ProtectUserName>False</ProtectUserName>
<ProtectPassword>True</ProtectPassword>
<ProtectURL>False</ProtectURL>
<ProtectNotes>False</ProtectNotes>
</MemoryProtection>
<RecycleBinEnabled>True</RecycleBinEnabled>
<RecycleBinUUID>AAAAAAAAAAAAAAAAAAAAAA==</RecycleBinUUID>
<RecycleBinChanged>2016-12-31T21:33:52Z</RecycleBinChanged>
<EntryTemplatesGroup>AAAAAAAAAAAAAAAAAAAAAA==</EntryTemplatesGroup>
<EntryTemplatesGroupChanged>2016-12-31T21:33:52Z</EntryTemplatesGroupChanged>
<HistoryMaxItems>10</HistoryMaxItems>
<HistoryMaxSize>6291456</HistoryMaxSize>
<LastSelectedGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastSelectedGroup>
<LastTopVisibleGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleGroup>
<Binaries />
<CustomData />
</Meta>
<Root>
<Group>
<UUID>KvS57lVwl13AfGFLwkvq4Q==</UUID>
<Name>Root</Name>
<Notes />
<IconID>48</IconID>
<Times>
<CreationTime>2016-12-31T21:33:52Z</CreationTime>
<LastModificationTime>2016-12-31T21:33:52Z</LastModificationTime>
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:33:52Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>1</UsageCount>
<LocationChanged>2016-12-31T21:33:52Z</LocationChanged>
</Times>
<IsExpanded>True</IsExpanded>
<DefaultAutoTypeSequence />
<EnableAutoType>null</EnableAutoType>
<EnableSearching>null</EnableSearching>
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
<Group>
<UUID>P0ParXgGMBW6caOL2YrhqQ==</UUID>
<Name>Folder2</Name>
<Notes>a note about the folder</Notes>
<IconID>48</IconID>
<Times>
<CreationTime>2016-12-31T21:43:30Z</CreationTime>
<LastModificationTime>2016-12-31T21:43:43Z</LastModificationTime>
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:43:30Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>1</UsageCount>
<LocationChanged>2016-12-31T21:43:43Z</LocationChanged>
</Times>
<IsExpanded>True</IsExpanded>
<DefaultAutoTypeSequence />
<EnableAutoType>null</EnableAutoType>
<EnableSearching>null</EnableSearching>
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
<Entry>
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
<IconID>1</IconID>
<ForegroundColor />
<BackgroundColor />
<OverrideURL />
<Tags />
<Times>
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
<LastModificationTime>2016-12-31T21:40:23Z</LastModificationTime>
<LastAccessTime>2016-12-31T21:40:23Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>2016-12-31T21:43:48Z</LocationChanged>
</Times>
<String>
<Key>att2</Key>
<Value>att2value</Value>
</String>
<String>
<Key>attr1</Key>
<Value>att1value
line1
line2</Value>
</String>
<String>
<Key>Notes</Key>
<Value>This is a note!!!
line1
line2</Value>
</String>
<String>
<Key>Password</Key>
<Value ProtectInMemory="True">googpass</Value>
</String>
<String>
<Key>Title</Key>
<Value>Google</Value>
</String>
<String>
<Key>URL</Key>
<Value>google.com</Value>
</String>
<String>
<Key>UserName</Key>
<Value>googleuser</Value>
</String>
<AutoType>
<Enabled>True</Enabled>
<DataTransferObfuscation>0</DataTransferObfuscation>
</AutoType>
<History>
<Entry>
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
<IconID>0</IconID>
<ForegroundColor />
<BackgroundColor />
<OverrideURL />
<Tags />
<Times>
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
<LastModificationTime>2016-12-31T21:34:40Z</LastModificationTime>
<LastAccessTime>2016-12-31T21:34:40Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>2016-12-31T21:34:40Z</LocationChanged>
</Times>
<String>
<Key>Notes</Key>
<Value>This is a note!!!
line1
line2</Value>
</String>
<String>
<Key>Password</Key>
<Value ProtectInMemory="True">googpass</Value>
</String>
<String>
<Key>Title</Key>
<Value>Google</Value>
</String>
<String>
<Key>URL</Key>
<Value>google.com</Value>
</String>
<String>
<Key>UserName</Key>
<Value>googleuser</Value>
</String>
<AutoType>
<Enabled>True</Enabled>
<DataTransferObfuscation>0</DataTransferObfuscation>
</AutoType>
</Entry>
</History>
</Entry>
</Group>
</Group>
<DeletedObjects />
</Root>`;

View File

@@ -1,184 +1,7 @@
import { KeePass2XmlImporter as Importer } from "@bitwarden/common/importers/keepass2-xml-importer";
import { FolderView } from "@bitwarden/common/models/view/folder.view";
const TestData = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<KeePassFile>
<Meta>
<Generator>KeePass</Generator>
<DatabaseName />
<DatabaseNameChanged>2016-12-31T21:33:52Z</DatabaseNameChanged>
<DatabaseDescription />
<DatabaseDescriptionChanged>2016-12-31T21:33:52Z</DatabaseDescriptionChanged>
<DefaultUserName />
<DefaultUserNameChanged>2016-12-31T21:33:52Z</DefaultUserNameChanged>
<MaintenanceHistoryDays>365</MaintenanceHistoryDays>
<Color />
<MasterKeyChanged>2016-12-31T21:33:59Z</MasterKeyChanged>
<MasterKeyChangeRec>-1</MasterKeyChangeRec>
<MasterKeyChangeForce>-1</MasterKeyChangeForce>
<MemoryProtection>
<ProtectTitle>False</ProtectTitle>
<ProtectUserName>False</ProtectUserName>
<ProtectPassword>True</ProtectPassword>
<ProtectURL>False</ProtectURL>
<ProtectNotes>False</ProtectNotes>
</MemoryProtection>
<RecycleBinEnabled>True</RecycleBinEnabled>
<RecycleBinUUID>AAAAAAAAAAAAAAAAAAAAAA==</RecycleBinUUID>
<RecycleBinChanged>2016-12-31T21:33:52Z</RecycleBinChanged>
<EntryTemplatesGroup>AAAAAAAAAAAAAAAAAAAAAA==</EntryTemplatesGroup>
<EntryTemplatesGroupChanged>2016-12-31T21:33:52Z</EntryTemplatesGroupChanged>
<HistoryMaxItems>10</HistoryMaxItems>
<HistoryMaxSize>6291456</HistoryMaxSize>
<LastSelectedGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastSelectedGroup>
<LastTopVisibleGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleGroup>
<Binaries />
<CustomData />
</Meta>
<Root>
<Group>
<UUID>KvS57lVwl13AfGFLwkvq4Q==</UUID>
<Name>Root</Name>
<Notes />
<IconID>48</IconID>
<Times>
<CreationTime>2016-12-31T21:33:52Z</CreationTime>
<LastModificationTime>2016-12-31T21:33:52Z</LastModificationTime>
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:33:52Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>1</UsageCount>
<LocationChanged>2016-12-31T21:33:52Z</LocationChanged>
</Times>
<IsExpanded>True</IsExpanded>
<DefaultAutoTypeSequence />
<EnableAutoType>null</EnableAutoType>
<EnableSearching>null</EnableSearching>
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
<Group>
<UUID>P0ParXgGMBW6caOL2YrhqQ==</UUID>
<Name>Folder2</Name>
<Notes>a note about the folder</Notes>
<IconID>48</IconID>
<Times>
<CreationTime>2016-12-31T21:43:30Z</CreationTime>
<LastModificationTime>2016-12-31T21:43:43Z</LastModificationTime>
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:43:30Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>1</UsageCount>
<LocationChanged>2016-12-31T21:43:43Z</LocationChanged>
</Times>
<IsExpanded>True</IsExpanded>
<DefaultAutoTypeSequence />
<EnableAutoType>null</EnableAutoType>
<EnableSearching>null</EnableSearching>
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
<Entry>
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
<IconID>1</IconID>
<ForegroundColor />
<BackgroundColor />
<OverrideURL />
<Tags />
<Times>
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
<LastModificationTime>2016-12-31T21:40:23Z</LastModificationTime>
<LastAccessTime>2016-12-31T21:40:23Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>2016-12-31T21:43:48Z</LocationChanged>
</Times>
<String>
<Key>att2</Key>
<Value>att2value</Value>
</String>
<String>
<Key>attr1</Key>
<Value>att1value
line1
line2</Value>
</String>
<String>
<Key>Notes</Key>
<Value>This is a note!!!
line1
line2</Value>
</String>
<String>
<Key>Password</Key>
<Value ProtectInMemory="True">googpass</Value>
</String>
<String>
<Key>Title</Key>
<Value>Google</Value>
</String>
<String>
<Key>URL</Key>
<Value>google.com</Value>
</String>
<String>
<Key>UserName</Key>
<Value>googleuser</Value>
</String>
<AutoType>
<Enabled>True</Enabled>
<DataTransferObfuscation>0</DataTransferObfuscation>
</AutoType>
<History>
<Entry>
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
<IconID>0</IconID>
<ForegroundColor />
<BackgroundColor />
<OverrideURL />
<Tags />
<Times>
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
<LastModificationTime>2016-12-31T21:34:40Z</LastModificationTime>
<LastAccessTime>2016-12-31T21:34:40Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>2016-12-31T21:34:40Z</LocationChanged>
</Times>
<String>
<Key>Notes</Key>
<Value>This is a note!!!
line1
line2</Value>
</String>
<String>
<Key>Password</Key>
<Value ProtectInMemory="True">googpass</Value>
</String>
<String>
<Key>Title</Key>
<Value>Google</Value>
</String>
<String>
<Key>URL</Key>
<Value>google.com</Value>
</String>
<String>
<Key>UserName</Key>
<Value>googleuser</Value>
</String>
<AutoType>
<Enabled>True</Enabled>
<DataTransferObfuscation>0</DataTransferObfuscation>
</AutoType>
</Entry>
</History>
</Entry>
</Group>
</Group>
<DeletedObjects />
</Root>
</KeePassFile>`;
import { TestData, TestData1, TestData2 } from "./keepass2-xml-importer-testdata";
describe("KeePass2 Xml Importer", () => {
it("should parse XML data", async () => {
@@ -186,4 +9,34 @@ describe("KeePass2 Xml Importer", () => {
const result = await importer.parse(TestData);
expect(result != null).toBe(true);
});
it("parse XML should contains folders", async () => {
const importer = new Importer();
const folder = new FolderView();
folder.name = "Folder2";
const actual = [folder];
const result = await importer.parse(TestData);
expect(result.folders).toEqual(actual);
});
it("parse XML should contains login details", async () => {
const importer = new Importer();
const result = await importer.parse(TestData);
expect(result.ciphers[0].login.uri != null).toBe(true);
expect(result.ciphers[0].login.username != null).toBe(true);
expect(result.ciphers[0].login.password != null).toBe(true);
});
it("should return error with missing root tag", async () => {
const importer = new Importer();
const result = await importer.parse(TestData1);
expect(result.errorMessage).toBe("Missing `KeePassFile > Root` node.");
});
it("should return error with missing KeePassFile tag", async () => {
const importer = new Importer();
const result = await importer.parse(TestData2);
expect(result.success).toBe(false);
});
});

View File

@@ -0,0 +1,77 @@
import { KeeperCsvImporter as Importer } from "@bitwarden/common/importers/keeper/keeper-csv-importer";
import { testData as TestData } from "./test-data/keeper-csv/testdata.csv";
describe("Keeper CSV Importer", () => {
let importer: Importer;
beforeEach(() => {
importer = new Importer();
});
it("should parse login data", async () => {
const result = await importer.parse(TestData);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.name).toEqual("Bar");
expect(cipher.login.username).toEqual("john.doe@example.com");
expect(cipher.login.password).toEqual("1234567890abcdef");
expect(cipher.login.uris.length).toEqual(1);
const uriView = cipher.login.uris.shift();
expect(uriView.uri).toEqual("https://example.com/");
expect(cipher.notes).toEqual("These are some notes.");
const cipher2 = result.ciphers.shift();
expect(cipher2.name).toEqual("Bar 1");
expect(cipher2.login.username).toEqual("john.doe1@example.com");
expect(cipher2.login.password).toEqual("234567890abcdef1");
expect(cipher2.login.uris.length).toEqual(1);
const uriView2 = cipher2.login.uris.shift();
expect(uriView2.uri).toEqual("https://an.example.com/");
expect(cipher2.notes).toBeNull();
const cipher3 = result.ciphers.shift();
expect(cipher3.name).toEqual("Bar 2");
expect(cipher3.login.username).toEqual("john.doe2@example.com");
expect(cipher3.login.password).toEqual("34567890abcdef12");
expect(cipher3.notes).toBeNull();
expect(cipher3.login.uris.length).toEqual(1);
const uriView3 = cipher3.login.uris.shift();
expect(uriView3.uri).toEqual("https://another.example.com/");
});
it("should import TOTP when present", async () => {
const result = await importer.parse(TestData);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.login.totp).toBeNull();
const cipher2 = result.ciphers.shift();
expect(cipher2.login.totp).toBeNull();
const cipher3 = result.ciphers.shift();
expect(cipher3.login.totp).toEqual(
"otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30"
);
});
it("should parse custom fields", async () => {
const result = await importer.parse(TestData);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.fields).toBeNull();
const cipher2 = result.ciphers.shift();
expect(cipher2.fields.length).toBe(2);
expect(cipher2.fields[0].name).toEqual("Account ID");
expect(cipher2.fields[0].value).toEqual("12345");
expect(cipher2.fields[1].name).toEqual("Org ID");
expect(cipher2.fields[1].value).toEqual("54321");
const cipher3 = result.ciphers.shift();
expect(cipher3.fields[0].name).toEqual("Account ID");
expect(cipher3.fields[0].value).toEqual("23456");
});
});

View File

@@ -0,0 +1,34 @@
import { PasskyJsonImporter as Importer } from "@bitwarden/common/importers/passky/passky-json-importer";
import { testData as EncryptedData } from "./test-data/passky-json/passky-encrypted.json";
import { testData as UnencryptedData } from "./test-data/passky-json/passky-unencrypted.json";
describe("Passky Json Importer", () => {
let importer: Importer;
beforeEach(() => {
importer = new Importer();
});
it("should not import encrypted backups", async () => {
const testDataJson = JSON.stringify(EncryptedData);
const result = await importer.parse(testDataJson);
expect(result != null).toBe(true);
expect(result.success).toBe(false);
expect(result.errorMessage).toBe("Unable to import an encrypted passky backup.");
});
it("should parse login data", async () => {
const testDataJson = JSON.stringify(UnencryptedData);
const result = await importer.parse(testDataJson);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.name).toEqual("https://bitwarden.com/");
expect(cipher.login.username).toEqual("testUser");
expect(cipher.login.password).toEqual("testPassword");
expect(cipher.login.uris.length).toEqual(1);
const uriView = cipher.login.uris.shift();
expect(uriView.uri).toEqual("https://bitwarden.com/");
expect(cipher.notes).toEqual("my notes");
});
});

View File

@@ -0,0 +1,4 @@
export const testData = `"Foo","Bar","john.doe@example.com","1234567890abcdef","https://example.com/","These are some notes.",""
"Foo","Bar 1","john.doe1@example.com","234567890abcdef1","https://an.example.com/","","","Account ID","12345","Org ID","54321"
"Foo\\Baz","Bar 2","john.doe2@example.com","34567890abcdef12","https://another.example.com/","","","Account ID","23456","TFC:Keeper","otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30"
`;

View File

@@ -0,0 +1,15 @@
import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types";
export const testData: PasskyJsonExport = {
encrypted: true,
passwords: [
{
website:
"w68uw6nCjUI3w7MNYsK7w6xqwqHDlXLCpsOEw4/Dq8KbIMK3w6fCvQJFFcOECsOlwprCqUAawqnDvsKbwrLCsCXCtcOlw4dp",
username: "bMKyUC0VPTx5woHCr8K9wpvDgGrClFAKw6VfJTgob8KVwqNoN8KIEA==",
password: "XcKxO2FjwqIJPkoHwqrDvcKtXcORw6TDlMOlw7TDvMORfmlNdMKOwq7DocO+",
message:
"w5jCrWTCgAV1RcO+DsOzw5zCvD5CwqLCtcKtw6sPwpbCmcOxwrfDlcOQw4h1wqomEhNtUkRgwrzCkxrClFBSHsO5wrfCrg==",
},
],
};

View File

@@ -0,0 +1,13 @@
import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types";
export const testData: PasskyJsonExport = {
encrypted: false,
passwords: [
{
website: "https://bitwarden.com/",
username: "testUser",
password: "testPassword",
message: "my notes",
},
],
};

View File

@@ -1,5 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { mock, MockProxy } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
@@ -67,33 +66,34 @@ export function identityTokenResponseFactory() {
}
describe("LogInStrategy", () => {
let cryptoService: SubstituteOf<CryptoService>;
let apiService: SubstituteOf<ApiService>;
let tokenService: SubstituteOf<TokenService>;
let appIdService: SubstituteOf<AppIdService>;
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
let messagingService: SubstituteOf<MessagingService>;
let logService: SubstituteOf<LogService>;
let stateService: SubstituteOf<StateService>;
let twoFactorService: SubstituteOf<TwoFactorService>;
let authService: SubstituteOf<AuthService>;
let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>;
let tokenService: MockProxy<TokenService>;
let appIdService: MockProxy<AppIdService>;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let messagingService: MockProxy<MessagingService>;
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let authService: MockProxy<AuthService>;
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
beforeEach(async () => {
cryptoService = Substitute.for<CryptoService>();
apiService = Substitute.for<ApiService>();
tokenService = Substitute.for<TokenService>();
appIdService = Substitute.for<AppIdService>();
platformUtilsService = Substitute.for<PlatformUtilsService>();
messagingService = Substitute.for<MessagingService>();
logService = Substitute.for<LogService>();
stateService = Substitute.for<StateService>();
twoFactorService = Substitute.for<TwoFactorService>();
authService = Substitute.for<AuthService>();
cryptoService = mock<CryptoService>();
apiService = mock<ApiService>();
tokenService = mock<TokenService>();
appIdService = mock<AppIdService>();
platformUtilsService = mock<PlatformUtilsService>();
messagingService = mock<MessagingService>();
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
authService = mock<AuthService>();
appIdService.getAppId().resolves(deviceId);
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.calledWith(accessToken).mockResolvedValue(decodedToken);
// The base class is abstract so we test it via PasswordLogInStrategy
passwordLogInStrategy = new PasswordLogInStrategy(
@@ -113,12 +113,11 @@ describe("LogInStrategy", () => {
describe("base class", () => {
it("sets the local environment after a successful login", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
tokenService.decodeToken(accessToken).resolves(decodedToken);
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
await passwordLogInStrategy.logIn(credentials);
stateService.received(1).addAccount(
expect(stateService.addAccount).toHaveBeenCalledWith(
new Account({
profile: {
...new AccountProfile(),
@@ -140,10 +139,9 @@ describe("LogInStrategy", () => {
},
})
);
cryptoService.received(1).setEncKey(encKey);
cryptoService.received(1).setEncPrivateKey(privateKey);
messagingService.received(1).send("loggedIn");
expect(cryptoService.setEncKey).toHaveBeenCalledWith(encKey);
expect(cryptoService.setEncPrivateKey).toHaveBeenCalledWith(privateKey);
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
});
it("builds AuthResult", async () => {
@@ -151,16 +149,16 @@ describe("LogInStrategy", () => {
tokenResponse.forcePasswordReset = true;
tokenResponse.resetMasterPassword = true;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
const result = await passwordLogInStrategy.logIn(credentials);
const expected = new AuthResult();
expected.forcePasswordReset = true;
expected.resetMasterPassword = true;
expected.twoFactorProviders = null;
expected.captchaSiteKey = "";
expect(result).toEqual(expected);
expect(result).toEqual({
forcePasswordReset: true,
resetMasterPassword: true,
twoFactorProviders: null,
captchaSiteKey: "",
} as AuthResult);
});
it("rejects login if CAPTCHA is required", async () => {
@@ -171,12 +169,12 @@ describe("LogInStrategy", () => {
HCaptcha_SiteKey: captchaSiteKey,
});
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
const result = await passwordLogInStrategy.logIn(credentials);
stateService.didNotReceive().addAccount(Arg.any());
messagingService.didNotReceive().send(Arg.any());
expect(stateService.addAccount).not.toHaveBeenCalled();
expect(messagingService.send).not.toHaveBeenCalled();
const expected = new AuthResult();
expected.captchaSiteKey = captchaSiteKey;
@@ -186,13 +184,20 @@ describe("LogInStrategy", () => {
it("makes a new public and private key for an old account", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.privateKey = null;
cryptoService.makeKeyPair(Arg.any()).resolves(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
await passwordLogInStrategy.logIn(credentials);
apiService.received(1).postAccountKeys(Arg.any());
// User key must be set before the new RSA keypair is generated, otherwise we can't decrypt the EncKey
expect(cryptoService.setKey).toHaveBeenCalled();
expect(cryptoService.makeKeyPair).toHaveBeenCalled();
expect(cryptoService.setKey.mock.invocationCallOrder[0]).toBeLessThan(
cryptoService.makeKeyPair.mock.invocationCallOrder[0]
);
expect(apiService.postAccountKeys).toHaveBeenCalled();
});
});
@@ -206,12 +211,12 @@ describe("LogInStrategy", () => {
error_description: "Two factor required.",
});
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
const result = await passwordLogInStrategy.logIn(credentials);
stateService.didNotReceive().addAccount(Arg.any());
messagingService.didNotReceive().send(Arg.any());
expect(stateService.addAccount).not.toHaveBeenCalled();
expect(messagingService.send).not.toHaveBeenCalled();
const expected = new AuthResult();
expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>();
@@ -220,26 +225,25 @@ describe("LogInStrategy", () => {
});
it("sends stored 2FA token to server", async () => {
tokenService.getTwoFactorToken().resolves(twoFactorToken);
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
tokenService.getTwoFactorToken.mockResolvedValue(twoFactorToken);
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
await passwordLogInStrategy.logIn(credentials);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const passwordTokenRequest = actual as any;
return (
passwordTokenRequest.twoFactor.provider === TwoFactorProviderType.Remember &&
passwordTokenRequest.twoFactor.token === twoFactorToken &&
passwordTokenRequest.twoFactor.remember === false
);
expect(apiService.postIdentityToken).toHaveBeenCalledWith(
expect.objectContaining({
twoFactor: {
provider: TwoFactorProviderType.Remember,
token: twoFactorToken,
remember: false,
} as TokenTwoFactorRequest,
})
);
});
it("sends 2FA token provided by user to server (single step)", async () => {
// This occurs if the user enters the 2FA code as an argument in the CLI
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
credentials.twoFactor = new TokenTwoFactorRequest(
twoFactorProviderType,
twoFactorToken,
@@ -248,14 +252,13 @@ describe("LogInStrategy", () => {
await passwordLogInStrategy.logIn(credentials);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const passwordTokenRequest = actual as any;
return (
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
passwordTokenRequest.twoFactor.token === twoFactorToken &&
passwordTokenRequest.twoFactor.remember === twoFactorRemember
);
expect(apiService.postIdentityToken).toHaveBeenCalledWith(
expect.objectContaining({
twoFactor: {
provider: twoFactorProviderType,
token: twoFactorToken,
remember: twoFactorRemember,
} as TokenTwoFactorRequest,
})
);
});
@@ -269,21 +272,20 @@ describe("LogInStrategy", () => {
null
);
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
await passwordLogInStrategy.logInTwoFactor(
new TokenTwoFactorRequest(twoFactorProviderType, twoFactorToken, twoFactorRemember),
null
);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const passwordTokenRequest = actual as any;
return (
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
passwordTokenRequest.twoFactor.token === twoFactorToken &&
passwordTokenRequest.twoFactor.remember === twoFactorRemember
);
expect(apiService.postIdentityToken).toHaveBeenCalledWith(
expect.objectContaining({
twoFactor: {
provider: twoFactorProviderType,
token: twoFactorToken,
remember: twoFactorRemember,
} as TokenTwoFactorRequest,
})
);
});

View File

@@ -1,5 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { mock, MockProxy } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
@@ -31,41 +30,43 @@ const preloginKey = new SymmetricCryptoKey(
const deviceId = Utils.newGuid();
describe("PasswordLogInStrategy", () => {
let cryptoService: SubstituteOf<CryptoService>;
let apiService: SubstituteOf<ApiService>;
let tokenService: SubstituteOf<TokenService>;
let appIdService: SubstituteOf<AppIdService>;
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
let messagingService: SubstituteOf<MessagingService>;
let logService: SubstituteOf<LogService>;
let stateService: SubstituteOf<StateService>;
let twoFactorService: SubstituteOf<TwoFactorService>;
let authService: SubstituteOf<AuthService>;
let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>;
let tokenService: MockProxy<TokenService>;
let appIdService: MockProxy<AppIdService>;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let messagingService: MockProxy<MessagingService>;
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let authService: MockProxy<AuthService>;
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
beforeEach(async () => {
cryptoService = Substitute.for<CryptoService>();
apiService = Substitute.for<ApiService>();
tokenService = Substitute.for<TokenService>();
appIdService = Substitute.for<AppIdService>();
platformUtilsService = Substitute.for<PlatformUtilsService>();
messagingService = Substitute.for<MessagingService>();
logService = Substitute.for<LogService>();
stateService = Substitute.for<StateService>();
twoFactorService = Substitute.for<TwoFactorService>();
authService = Substitute.for<AuthService>();
cryptoService = mock<CryptoService>();
apiService = mock<ApiService>();
tokenService = mock<TokenService>();
appIdService = mock<AppIdService>();
platformUtilsService = mock<PlatformUtilsService>();
messagingService = mock<MessagingService>();
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
authService = mock<AuthService>();
appIdService.getAppId().resolves(deviceId);
tokenService.getTwoFactorToken().resolves(null);
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.mockResolvedValue({});
authService.makePreloginKey(Arg.any(), Arg.any()).resolves(preloginKey);
authService.makePreloginKey.mockResolvedValue(preloginKey);
cryptoService.hashPassword(masterPassword, Arg.any()).resolves(hashedPassword);
cryptoService
.hashPassword(masterPassword, Arg.any(), HashPurpose.LocalAuthorization)
.resolves(localHashedPassword);
cryptoService.hashPassword
.calledWith(masterPassword, expect.anything(), undefined)
.mockResolvedValue(hashedPassword);
cryptoService.hashPassword
.calledWith(masterPassword, expect.anything(), HashPurpose.LocalAuthorization)
.mockResolvedValue(localHashedPassword);
passwordLogInStrategy = new PasswordLogInStrategy(
cryptoService,
@@ -81,23 +82,24 @@ describe("PasswordLogInStrategy", () => {
);
credentials = new PasswordLogInCredentials(email, masterPassword);
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
});
it("sends master password credentials to the server", async () => {
await passwordLogInStrategy.logIn(credentials);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const passwordTokenRequest = actual as any; // Need to access private fields
return (
passwordTokenRequest.email === email &&
passwordTokenRequest.masterPasswordHash === hashedPassword &&
passwordTokenRequest.device.identifier === deviceId &&
passwordTokenRequest.twoFactor.provider == null &&
passwordTokenRequest.twoFactor.token == null &&
passwordTokenRequest.captchaResponse == null
);
expect(apiService.postIdentityToken).toHaveBeenCalledWith(
expect.objectContaining({
email: email,
masterPasswordHash: hashedPassword,
device: expect.objectContaining({
identifier: deviceId,
}),
twoFactor: expect.objectContaining({
provider: null,
token: null,
}),
captchaResponse: undefined,
})
);
});
@@ -105,7 +107,7 @@ describe("PasswordLogInStrategy", () => {
it("sets the local environment after a successful login", async () => {
await passwordLogInStrategy.logIn(credentials);
cryptoService.received(1).setKey(preloginKey);
cryptoService.received(1).setKeyHash(localHashedPassword);
expect(cryptoService.setKey).toHaveBeenCalledWith(preloginKey);
expect(cryptoService.setKeyHash).toHaveBeenCalledWith(localHashedPassword);
});
});

View File

@@ -1,5 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { mock, MockProxy } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
@@ -18,23 +17,21 @@ import { SsoLogInCredentials } from "@bitwarden/common/models/domain/log-in-cred
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
describe("SsoLogInStrategy", () => {
let cryptoService: SubstituteOf<CryptoService>;
let apiService: SubstituteOf<ApiService>;
let tokenService: SubstituteOf<TokenService>;
let appIdService: SubstituteOf<AppIdService>;
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
let messagingService: SubstituteOf<MessagingService>;
let logService: SubstituteOf<LogService>;
let keyConnectorService: SubstituteOf<KeyConnectorService>;
let stateService: SubstituteOf<StateService>;
let twoFactorService: SubstituteOf<TwoFactorService>;
let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>;
let tokenService: MockProxy<TokenService>;
let appIdService: MockProxy<AppIdService>;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let messagingService: MockProxy<MessagingService>;
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let keyConnectorService: MockProxy<KeyConnectorService>;
let ssoLogInStrategy: SsoLogInStrategy;
let credentials: SsoLogInCredentials;
const deviceId = Utils.newGuid();
const encKey = "ENC_KEY";
const privateKey = "PRIVATE_KEY";
const keyConnectorUrl = "KEY_CONNECTOR_URL";
const ssoCode = "SSO_CODE";
@@ -43,19 +40,20 @@ describe("SsoLogInStrategy", () => {
const ssoOrgId = "SSO_ORG_ID";
beforeEach(async () => {
cryptoService = Substitute.for<CryptoService>();
apiService = Substitute.for<ApiService>();
tokenService = Substitute.for<TokenService>();
appIdService = Substitute.for<AppIdService>();
platformUtilsService = Substitute.for<PlatformUtilsService>();
messagingService = Substitute.for<MessagingService>();
logService = Substitute.for<LogService>();
stateService = Substitute.for<StateService>();
keyConnectorService = Substitute.for<KeyConnectorService>();
twoFactorService = Substitute.for<TwoFactorService>();
cryptoService = mock<CryptoService>();
apiService = mock<ApiService>();
tokenService = mock<TokenService>();
appIdService = mock<AppIdService>();
platformUtilsService = mock<PlatformUtilsService>();
messagingService = mock<MessagingService>();
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
keyConnectorService = mock<KeyConnectorService>();
tokenService.getTwoFactorToken().resolves(null);
appIdService.getAppId().resolves(deviceId);
tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.mockResolvedValue({});
ssoLogInStrategy = new SsoLogInStrategy(
cryptoService,
@@ -73,21 +71,22 @@ describe("SsoLogInStrategy", () => {
});
it("sends SSO information to server", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
await ssoLogInStrategy.logIn(credentials);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const ssoTokenRequest = actual as any;
return (
ssoTokenRequest.code === ssoCode &&
ssoTokenRequest.codeVerifier === ssoCodeVerifier &&
ssoTokenRequest.redirectUri === ssoRedirectUrl &&
ssoTokenRequest.device.identifier === deviceId &&
ssoTokenRequest.twoFactor.provider == null &&
ssoTokenRequest.twoFactor.token == null
);
expect(apiService.postIdentityToken).toHaveBeenCalledWith(
expect.objectContaining({
code: ssoCode,
codeVerifier: ssoCodeVerifier,
redirectUri: ssoRedirectUrl,
device: expect.objectContaining({
identifier: deviceId,
}),
twoFactor: expect.objectContaining({
provider: null,
token: null,
}),
})
);
});
@@ -95,23 +94,23 @@ describe("SsoLogInStrategy", () => {
it("does not set keys for new SSO user flow", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.key = null;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
await ssoLogInStrategy.logIn(credentials);
cryptoService.didNotReceive().setEncPrivateKey(privateKey);
cryptoService.didNotReceive().setEncKey(encKey);
expect(cryptoService.setEncPrivateKey).not.toHaveBeenCalled();
expect(cryptoService.setEncKey).not.toHaveBeenCalled();
});
it("gets and sets KeyConnector key for enrolled user", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.keyConnectorUrl = keyConnectorUrl;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
await ssoLogInStrategy.logIn(credentials);
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl);
});
it("converts new SSO user to Key Connector on first login", async () => {
@@ -119,10 +118,13 @@ describe("SsoLogInStrategy", () => {
tokenResponse.keyConnectorUrl = keyConnectorUrl;
tokenResponse.key = null;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
await ssoLogInStrategy.logIn(credentials);
keyConnectorService.received(1).convertNewSsoUserToKeyConnector(tokenResponse, ssoOrgId);
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
tokenResponse,
ssoOrgId
);
});
});

View File

@@ -1,5 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { mock, MockProxy } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
@@ -19,17 +18,17 @@ import { UserApiLogInCredentials } from "@bitwarden/common/models/domain/log-in-
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
describe("UserApiLogInStrategy", () => {
let cryptoService: SubstituteOf<CryptoService>;
let apiService: SubstituteOf<ApiService>;
let tokenService: SubstituteOf<TokenService>;
let appIdService: SubstituteOf<AppIdService>;
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
let messagingService: SubstituteOf<MessagingService>;
let logService: SubstituteOf<LogService>;
let environmentService: SubstituteOf<EnvironmentService>;
let keyConnectorService: SubstituteOf<KeyConnectorService>;
let stateService: SubstituteOf<StateService>;
let twoFactorService: SubstituteOf<TwoFactorService>;
let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>;
let tokenService: MockProxy<TokenService>;
let appIdService: MockProxy<AppIdService>;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let messagingService: MockProxy<MessagingService>;
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let keyConnectorService: MockProxy<KeyConnectorService>;
let environmentService: MockProxy<EnvironmentService>;
let apiLogInStrategy: UserApiLogInStrategy;
let credentials: UserApiLogInCredentials;
@@ -40,20 +39,21 @@ describe("UserApiLogInStrategy", () => {
const apiClientSecret = "API_CLIENT_SECRET";
beforeEach(async () => {
cryptoService = Substitute.for<CryptoService>();
apiService = Substitute.for<ApiService>();
tokenService = Substitute.for<TokenService>();
appIdService = Substitute.for<AppIdService>();
platformUtilsService = Substitute.for<PlatformUtilsService>();
messagingService = Substitute.for<MessagingService>();
logService = Substitute.for<LogService>();
environmentService = Substitute.for<EnvironmentService>();
stateService = Substitute.for<StateService>();
keyConnectorService = Substitute.for<KeyConnectorService>();
twoFactorService = Substitute.for<TwoFactorService>();
cryptoService = mock<CryptoService>();
apiService = mock<ApiService>();
tokenService = mock<TokenService>();
appIdService = mock<AppIdService>();
platformUtilsService = mock<PlatformUtilsService>();
messagingService = mock<MessagingService>();
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
keyConnectorService = mock<KeyConnectorService>();
environmentService = mock<EnvironmentService>();
appIdService.getAppId().resolves(deviceId);
tokenService.getTwoFactorToken().resolves(null);
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.getTwoFactorToken.mockResolvedValue(null);
tokenService.decodeToken.mockResolvedValue({});
apiLogInStrategy = new UserApiLogInStrategy(
cryptoService,
@@ -73,43 +73,43 @@ describe("UserApiLogInStrategy", () => {
});
it("sends api key credentials to the server", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
await apiLogInStrategy.logIn(credentials);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const apiTokenRequest = actual as any;
return (
apiTokenRequest.clientId === apiClientId &&
apiTokenRequest.clientSecret === apiClientSecret &&
apiTokenRequest.device.identifier === deviceId &&
apiTokenRequest.twoFactor.provider == null &&
apiTokenRequest.twoFactor.token == null &&
apiTokenRequest.captchaResponse == null
);
expect(apiService.postIdentityToken).toHaveBeenCalledWith(
expect.objectContaining({
clientId: apiClientId,
clientSecret: apiClientSecret,
device: expect.objectContaining({
identifier: deviceId,
}),
twoFactor: expect.objectContaining({
provider: null,
token: null,
}),
})
);
});
it("sets the local environment after a successful login", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
await apiLogInStrategy.logIn(credentials);
stateService.received(1).setApiKeyClientId(apiClientId);
stateService.received(1).setApiKeyClientSecret(apiClientSecret);
stateService.received(1).addAccount(Arg.any());
expect(stateService.setApiKeyClientId).toHaveBeenCalledWith(apiClientId);
expect(stateService.setApiKeyClientSecret).toHaveBeenCalledWith(apiClientSecret);
expect(stateService.addAccount).toHaveBeenCalled();
});
it("gets and sets the Key Connector key from environmentUrl", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.apiUseKeyConnector = true;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
environmentService.getKeyConnectorUrl().returns(keyConnectorUrl);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
environmentService.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
await apiLogInStrategy.logIn(credentials);
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl);
});
});

View File

@@ -241,4 +241,89 @@ describe("Utils Service", () => {
expect(Utils.fromByteStringToArray(null)).toEqual(null);
});
});
describe("mapToRecord", () => {
it("should handle null", () => {
expect(Utils.mapToRecord(null)).toEqual(null);
});
it("should handle empty map", () => {
expect(Utils.mapToRecord(new Map())).toEqual({});
});
it("should handle convert a Map to a Record", () => {
const map = new Map([
["key1", "value1"],
["key2", "value2"],
]);
expect(Utils.mapToRecord(map)).toEqual({ key1: "value1", key2: "value2" });
});
it("should handle convert a Map to a Record with non-string keys", () => {
const map = new Map([
[1, "value1"],
[2, "value2"],
]);
const result = Utils.mapToRecord(map);
expect(result).toEqual({ 1: "value1", 2: "value2" });
expect(Utils.recordToMap(result)).toEqual(map);
});
it("should not convert an object if it's not a map", () => {
const obj = { key1: "value1", key2: "value2" };
expect(Utils.mapToRecord(obj as any)).toEqual(obj);
});
});
describe("recordToMap", () => {
it("should handle null", () => {
expect(Utils.recordToMap(null)).toEqual(null);
});
it("should handle empty record", () => {
expect(Utils.recordToMap({})).toEqual(new Map());
});
it("should handle convert a Record to a Map", () => {
const record = { key1: "value1", key2: "value2" };
expect(Utils.recordToMap(record)).toEqual(new Map(Object.entries(record)));
});
it("should handle convert a Record to a Map with non-string keys", () => {
const record = { 1: "value1", 2: "value2" };
const result = Utils.recordToMap(record);
expect(result).toEqual(
new Map([
[1, "value1"],
[2, "value2"],
])
);
expect(Utils.mapToRecord(result)).toEqual(record);
});
it("should not convert an object if already a map", () => {
const map = new Map([
["key1", "value1"],
["key2", "value2"],
]);
expect(Utils.recordToMap(map as any)).toEqual(map);
});
});
describe("encodeRFC3986URIComponent", () => {
it("returns input string with expected encoded chars", () => {
expect(Utils.encodeRFC3986URIComponent("test'user@example.com")).toBe(
"test%27user%40example.com"
);
expect(Utils.encodeRFC3986URIComponent("(test)user@example.com")).toBe(
"%28test%29user%40example.com"
);
expect(Utils.encodeRFC3986URIComponent("testuser!@example.com")).toBe(
"testuser%21%40example.com"
);
expect(Utils.encodeRFC3986URIComponent("Test*User@example.com")).toBe(
"Test%2AUser%40example.com"
);
});
});
});

View File

@@ -1,6 +1,5 @@
// eslint-disable-next-line no-restricted-imports
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { BehaviorSubject } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
@@ -8,13 +7,15 @@ import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { KdfType } from "@bitwarden/common/enums/kdfType";
import { KdfType, DEFAULT_KDF_ITERATIONS } from "@bitwarden/common/enums/kdfType";
import { Utils } from "@bitwarden/common/misc/utils";
import { Cipher } from "@bitwarden/common/models/domain/cipher";
import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { Folder } from "@bitwarden/common/models/domain/folder";
import { Login } from "@bitwarden/common/models/domain/login";
import { CipherWithIdExport as CipherExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/models/view/folder.view";
import { LoginView } from "@bitwarden/common/models/view/login.view";
import { ExportService } from "@bitwarden/common/services/export.service";
@@ -32,6 +33,10 @@ const UserCipherDomains = [
generateCipherDomain(true),
];
const UserFolderViews = [generateFolderView(), generateFolderView()];
const UserFolders = [generateFolder(), generateFolder()];
function generateCipherView(deleted: boolean) {
return BuildTestObject(
{
@@ -72,6 +77,26 @@ function generateCipherDomain(deleted: boolean) {
);
}
function generateFolderView() {
return BuildTestObject(
{
id: GetUniqueString("id"),
name: GetUniqueString("name"),
revisionDate: new Date(),
},
FolderView
);
}
function generateFolder() {
const actual = Folder.fromJSON({
revisionDate: new Date("2022-08-04T01:06:40.441Z").toISOString(),
name: "name",
id: "id",
});
return actual;
}
function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string) {
const actual = JSON.stringify(JSON.parse(jsonResult).items);
const items: CipherExport[] = [];
@@ -84,6 +109,34 @@ function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string
expect(actual).toEqual(JSON.stringify(items));
}
function expectEqualFolderViews(folderviews: FolderView[] | Folder[], jsonResult: string) {
const actual = JSON.stringify(JSON.parse(jsonResult).folders);
const folders: FolderResponse[] = [];
folderviews.forEach((c) => {
const folder = new FolderResponse();
folder.id = c.id;
folder.name = c.name.toString();
folders.push(folder);
});
expect(actual.length).toBeGreaterThan(0);
expect(actual).toEqual(JSON.stringify(folders));
}
function expectEqualFolders(folders: Folder[], jsonResult: string) {
const actual = JSON.stringify(JSON.parse(jsonResult).folders);
const items: Folder[] = [];
folders.forEach((c) => {
const item = new Folder();
item.id = c.id;
item.name = c.name;
items.push(item);
});
expect(actual.length).toBeGreaterThan(0);
expect(actual).toEqual(JSON.stringify(items));
}
describe("ExportService", () => {
let exportService: ExportService;
let apiService: SubstituteOf<ApiService>;
@@ -99,8 +152,8 @@ describe("ExportService", () => {
folderService = Substitute.for<FolderService>();
cryptoService = Substitute.for<CryptoService>();
folderService.folderViews$.returns(new BehaviorSubject([]));
folderService.folders$.returns(new BehaviorSubject([]));
folderService.getAllDecryptedFromState().resolves(UserFolderViews);
folderService.getAllFromState().resolves(UserFolders);
exportService = new ExportService(
folderService,
@@ -179,7 +232,7 @@ describe("ExportService", () => {
});
it("specifies kdfIterations", () => {
expect(exportObject.kdfIterations).toEqual(100000);
expect(exportObject.kdfIterations).toEqual(DEFAULT_KDF_ITERATIONS);
});
it("has kdfType", () => {
@@ -208,4 +261,25 @@ describe("ExportService", () => {
});
});
});
it("exported unencrypted object contains folders", async () => {
cipherService.getAllDecrypted().resolves(UserCipherViews.slice(0, 1));
await folderService.getAllDecryptedFromState();
const actual = await exportService.getExport("json");
expectEqualFolderViews(UserFolderViews, actual);
});
it("exported encrypted json contains folders", async () => {
cipherService.getAll().resolves(UserCipherDomains.slice(0, 1));
await folderService.getAllFromState();
const actual = await exportService.getExport("encrypted_json");
expectEqualFolders(UserFolders, actual);
});
});
export class FolderResponse {
id: string = null;
name: string = null;
}

View File

@@ -1,12 +1,9 @@
import { MockProxy, mock, any, mockClear, matches } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, Subject } from "rxjs";
import { MockProxy, mock, any, mockClear } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncNotifierService } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction";
import { OrganizationData } from "@bitwarden/common/models/data/organization.data";
import { SyncResponse } from "@bitwarden/common/models/response/sync.response";
import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
import { SyncEventArgs } from "@bitwarden/common/types/syncEventArgs";
describe("Organization Service", () => {
let organizationService: OrganizationService;
@@ -14,8 +11,6 @@ describe("Organization Service", () => {
let stateService: MockProxy<StateService>;
let activeAccount: BehaviorSubject<string>;
let activeAccountUnlocked: BehaviorSubject<boolean>;
let syncNotifierService: MockProxy<SyncNotifierService>;
let sync: Subject<SyncEventArgs>;
const resetStateService = async (
customizeStateService: (stateService: MockProxy<StateService>) => void
@@ -25,7 +20,7 @@ describe("Organization Service", () => {
stateService.activeAccount$ = activeAccount;
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
customizeStateService(stateService);
organizationService = new OrganizationService(stateService, syncNotifierService);
organizationService = new OrganizationService(stateService);
await new Promise((r) => setTimeout(r, 50));
};
@@ -41,12 +36,7 @@ describe("Organization Service", () => {
"1": organizationData("1", "Test Org"),
});
sync = new Subject<SyncEventArgs>();
syncNotifierService = mock<SyncNotifierService>();
syncNotifierService.sync$ = sync;
organizationService = new OrganizationService(stateService, syncNotifierService);
organizationService = new OrganizationService(stateService);
});
afterEach(() => {
@@ -169,36 +159,6 @@ describe("Organization Service", () => {
});
});
describe("syncEvent works", () => {
it("Complete event updates data", async () => {
sync.next({
status: "Completed",
successfully: true,
data: new SyncResponse({
profile: {
organizations: [
{
id: "1",
name: "Updated Name",
},
],
},
}),
});
await new Promise((r) => setTimeout(r, 500));
expect(stateService.setOrganizations).toHaveBeenCalledTimes(1);
expect(stateService.setOrganizations).toHaveBeenLastCalledWith(
matches((organizationData: { [id: string]: OrganizationData }) => {
const organization = organizationData["1"];
return organization?.name === "Updated Name";
})
);
});
});
function organizationData(id: string, name: string) {
const data = new OrganizationData({} as any);
data.id = id;

View File

@@ -0,0 +1,8 @@
import { Observable } from "rxjs";
import { ProfileResponse } from "../../models/response/profile.response";
export abstract class AvatarUpdateService {
avatarUpdate$ = new Observable<string | null>();
abstract pushUpdate(color: string): Promise<ProfileResponse | void>;
abstract loadColorFromState(): Promise<string | null>;
}

View File

@@ -11,6 +11,7 @@ import { CipherCreateRequest } from "../models/request/cipher-create.request";
import { CipherPartialRequest } from "../models/request/cipher-partial.request";
import { CipherShareRequest } from "../models/request/cipher-share.request";
import { CipherRequest } from "../models/request/cipher.request";
import { CollectionBulkDeleteRequest } from "../models/request/collection-bulk-delete.request";
import { CollectionRequest } from "../models/request/collection.request";
import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
import { DeviceVerificationRequest } from "../models/request/device-verification.request";
@@ -22,7 +23,6 @@ import { EmergencyAccessInviteRequest } from "../models/request/emergency-access
import { EmergencyAccessPasswordRequest } from "../models/request/emergency-access-password.request";
import { EmergencyAccessUpdateRequest } from "../models/request/emergency-access-update.request";
import { EventRequest } from "../models/request/event.request";
import { GroupRequest } from "../models/request/group.request";
import { IapCheckRequest } from "../models/request/iap-check.request";
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
import { SsoTokenRequest } from "../models/request/identity-token/sso-token.request";
@@ -34,15 +34,6 @@ import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user
import { KeysRequest } from "../models/request/keys.request";
import { OrganizationConnectionRequest } from "../models/request/organization-connection.request";
import { OrganizationImportRequest } from "../models/request/organization-import.request";
import { OrganizationUserAcceptRequest } from "../models/request/organization-user-accept.request";
import { OrganizationUserBulkConfirmRequest } from "../models/request/organization-user-bulk-confirm.request";
import { OrganizationUserBulkRequest } from "../models/request/organization-user-bulk.request";
import { OrganizationUserConfirmRequest } from "../models/request/organization-user-confirm.request";
import { OrganizationUserInviteRequest } from "../models/request/organization-user-invite.request";
import { OrganizationUserResetPasswordEnrollmentRequest } from "../models/request/organization-user-reset-password-enrollment.request";
import { OrganizationUserResetPasswordRequest } from "../models/request/organization-user-reset-password.request";
import { OrganizationUserUpdateGroupsRequest } from "../models/request/organization-user-update-groups.request";
import { OrganizationUserUpdateRequest } from "../models/request/organization-user-update.request";
import { OrganizationSponsorshipCreateRequest } from "../models/request/organization/organization-sponsorship-create.request";
import { OrganizationSponsorshipRedeemRequest } from "../models/request/organization/organization-sponsorship-redeem.request";
import { PasswordHintRequest } from "../models/request/password-hint.request";
@@ -71,6 +62,7 @@ import { TaxInfoUpdateRequest } from "../models/request/tax-info-update.request"
import { TwoFactorEmailRequest } from "../models/request/two-factor-email.request";
import { TwoFactorProviderRequest } from "../models/request/two-factor-provider.request";
import { TwoFactorRecoveryRequest } from "../models/request/two-factor-recovery.request";
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
import { UpdateDomainsRequest } from "../models/request/update-domains.request";
import { UpdateKeyRequest } from "../models/request/update-key.request";
import { UpdateProfileRequest } from "../models/request/update-profile.request";
@@ -93,7 +85,7 @@ import { BillingPaymentResponse } from "../models/response/billing-payment.respo
import { BreachAccountResponse } from "../models/response/breach-account.response";
import { CipherResponse } from "../models/response/cipher.response";
import {
CollectionGroupDetailsResponse,
CollectionAccessDetailsResponse,
CollectionResponse,
} from "../models/response/collection.response";
import { DeviceVerificationResponse } from "../models/response/device-verification.response";
@@ -105,7 +97,6 @@ import {
EmergencyAccessViewResponse,
} from "../models/response/emergency-access.response";
import { EventResponse } from "../models/response/event.response";
import { GroupDetailsResponse, GroupResponse } from "../models/response/group.response";
import { IdentityCaptchaResponse } from "../models/response/identity-captcha.response";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
@@ -117,13 +108,6 @@ import {
} from "../models/response/organization-connection.response";
import { OrganizationExportResponse } from "../models/response/organization-export.response";
import { OrganizationSponsorshipSyncStatusResponse } from "../models/response/organization-sponsorship-sync-status.response";
import { OrganizationUserBulkPublicKeyResponse } from "../models/response/organization-user-bulk-public-key.response";
import { OrganizationUserBulkResponse } from "../models/response/organization-user-bulk.response";
import {
OrganizationUserDetailsResponse,
OrganizationUserUserDetailsResponse,
OrganizationUserResetPasswordDetailsReponse,
} from "../models/response/organization-user.response";
import { PaymentResponse } from "../models/response/payment.response";
import { PlanResponse } from "../models/response/plan.response";
import { PolicyResponse } from "../models/response/policy.response";
@@ -136,8 +120,8 @@ import {
import { ProviderUserBulkPublicKeyResponse } from "../models/response/provider/provider-user-bulk-public-key.response";
import { ProviderUserBulkResponse } from "../models/response/provider/provider-user-bulk.response";
import {
ProviderUserUserDetailsResponse,
ProviderUserResponse,
ProviderUserUserDetailsResponse,
} from "../models/response/provider/provider-user.response";
import { ProviderResponse } from "../models/response/provider/provider.response";
import { SelectionReadOnlyResponse } from "../models/response/selection-read-only.response";
@@ -156,13 +140,18 @@ import { TwoFactorEmailResponse } from "../models/response/two-factor-email.resp
import { TwoFactorProviderResponse } from "../models/response/two-factor-provider.response";
import { TwoFactorRecoverResponse } from "../models/response/two-factor-recover.response";
import {
TwoFactorWebAuthnResponse,
ChallengeResponse,
TwoFactorWebAuthnResponse,
} from "../models/response/two-factor-web-authn.response";
import { TwoFactorYubiKeyResponse } from "../models/response/two-factor-yubi-key.response";
import { UserKeyResponse } from "../models/response/user-key.response";
import { SendAccessView } from "../models/view/send-access.view";
/**
* @deprecated The `ApiService` class is deprecated and calls should be extracted into individual
* api services. The `send` method is still allowed to be used within api services. For background
* of this decision please read https://contributing.bitwarden.com/architecture/adr/refactor-api-service.
*/
export abstract class ApiService {
send: (
method: "GET" | "POST" | "PUT" | "DELETE",
@@ -183,6 +172,7 @@ export abstract class ApiService {
getUserSubscription: () => Promise<SubscriptionResponse>;
getTaxInfo: () => Promise<TaxInfoResponse>;
putProfile: (request: UpdateProfileRequest) => Promise<ProfileResponse>;
putAvatar: (request: UpdateAvatarRequest) => Promise<ProfileResponse>;
putTaxInfo: (request: TaxInfoUpdateRequest) => Promise<any>;
postPrelogin: (request: PreloginRequest) => Promise<PreloginResponse>;
postEmailToken: (request: EmailTokenRequest) => Promise<any>;
@@ -313,13 +303,16 @@ export abstract class ApiService {
) => Promise<AttachmentUploadDataResponse>;
postAttachmentFile: (id: string, attachmentId: string, data: FormData) => Promise<any>;
getCollectionDetails: (
organizationId: string,
id: string
) => Promise<CollectionGroupDetailsResponse>;
getUserCollections: () => Promise<ListResponse<CollectionResponse>>;
getCollections: (organizationId: string) => Promise<ListResponse<CollectionResponse>>;
getCollectionUsers: (organizationId: string, id: string) => Promise<SelectionReadOnlyResponse[]>;
getCollectionAccessDetails: (
organizationId: string,
id: string
) => Promise<CollectionAccessDetailsResponse>;
getManyCollectionsWithAccessDetails: (
orgId: string
) => Promise<ListResponse<CollectionAccessDetailsResponse>>;
postCollection: (
organizationId: string,
request: CollectionRequest
@@ -335,97 +328,17 @@ export abstract class ApiService {
request: CollectionRequest
) => Promise<CollectionResponse>;
deleteCollection: (organizationId: string, id: string) => Promise<any>;
deleteManyCollections: (request: CollectionBulkDeleteRequest) => Promise<any>;
deleteCollectionUser: (
organizationId: string,
id: string,
organizationUserId: string
) => Promise<any>;
getGroupDetails: (organizationId: string, id: string) => Promise<GroupDetailsResponse>;
getGroups: (organizationId: string) => Promise<ListResponse<GroupResponse>>;
getGroupUsers: (organizationId: string, id: string) => Promise<string[]>;
postGroup: (organizationId: string, request: GroupRequest) => Promise<GroupResponse>;
putGroup: (organizationId: string, id: string, request: GroupRequest) => Promise<GroupResponse>;
putGroupUsers: (organizationId: string, id: string, request: string[]) => Promise<any>;
deleteGroup: (organizationId: string, id: string) => Promise<any>;
deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>;
getOrganizationUser: (
organizationId: string,
id: string
) => Promise<OrganizationUserDetailsResponse>;
getOrganizationUserGroups: (organizationId: string, id: string) => Promise<string[]>;
getOrganizationUsers: (
organizationId: string
) => Promise<ListResponse<OrganizationUserUserDetailsResponse>>;
getOrganizationUserResetPasswordDetails: (
organizationId: string,
id: string
) => Promise<OrganizationUserResetPasswordDetailsReponse>;
postOrganizationUserInvite: (
organizationId: string,
request: OrganizationUserInviteRequest
) => Promise<any>;
postOrganizationUserReinvite: (organizationId: string, id: string) => Promise<any>;
postManyOrganizationUserReinvite: (
organizationId: string,
request: OrganizationUserBulkRequest
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
postOrganizationUserAccept: (
organizationId: string,
id: string,
request: OrganizationUserAcceptRequest
) => Promise<any>;
postOrganizationUserConfirm: (
organizationId: string,
id: string,
request: OrganizationUserConfirmRequest
) => Promise<any>;
postOrganizationUsersPublicKey: (
organizationId: string,
request: OrganizationUserBulkRequest
) => Promise<ListResponse<OrganizationUserBulkPublicKeyResponse>>;
postOrganizationUserBulkConfirm: (
organizationId: string,
request: OrganizationUserBulkConfirmRequest
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
putOrganizationUser: (
organizationId: string,
id: string,
request: OrganizationUserUpdateRequest
) => Promise<any>;
putOrganizationUserGroups: (
organizationId: string,
id: string,
request: OrganizationUserUpdateGroupsRequest
) => Promise<any>;
putOrganizationUserResetPasswordEnrollment: (
organizationId: string,
userId: string,
request: OrganizationUserResetPasswordEnrollmentRequest
) => Promise<void>;
putOrganizationUserResetPassword: (
organizationId: string,
id: string,
request: OrganizationUserResetPasswordRequest
) => Promise<any>;
deleteOrganizationUser: (organizationId: string, id: string) => Promise<any>;
deleteManyOrganizationUsers: (
organizationId: string,
request: OrganizationUserBulkRequest
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
revokeOrganizationUser: (organizationId: string, id: string) => Promise<any>;
revokeManyOrganizationUsers: (
organizationId: string,
request: OrganizationUserBulkRequest
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
restoreOrganizationUser: (organizationId: string, id: string) => Promise<any>;
restoreManyOrganizationUsers: (
organizationId: string,
request: OrganizationUserBulkRequest
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
getSync: () => Promise<SyncResponse>;
postPublicImportDirectory: (request: OrganizationImportRequest) => Promise<any>;

View File

@@ -66,8 +66,8 @@ export abstract class CipherService {
deleteManyWithServer: (ids: string[]) => Promise<any>;
deleteAttachment: (id: string, attachmentId: string) => Promise<void>;
deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise<void>;
sortCiphersByLastUsed: (a: any, b: any) => number;
sortCiphersByLastUsedThenName: (a: any, b: any) => number;
sortCiphersByLastUsed: (a: CipherView, b: CipherView) => number;
sortCiphersByLastUsedThenName: (a: CipherView, b: CipherView) => number;
getLocaleSortingFunction: () => (a: CipherView, b: CipherView) => number;
softDelete: (id: string | string[]) => Promise<any>;
softDeleteWithServer: (id: string) => Promise<any>;

View File

@@ -1,12 +0,0 @@
import { EventType } from "../enums/eventType";
export abstract class EventService {
collect: (
eventType: EventType,
cipherId?: string,
uploadImmediately?: boolean,
organizationId?: string
) => Promise<any>;
uploadEvents: (userId?: string) => Promise<any>;
clearEvents: (userId?: string) => Promise<any>;
}

View File

@@ -0,0 +1,10 @@
import { EventType } from "../../enums/eventType";
export abstract class EventCollectionService {
collect: (
eventType: EventType,
cipherId?: string,
uploadImmediately?: boolean,
organizationId?: string
) => Promise<any>;
}

View File

@@ -0,0 +1,3 @@
export abstract class EventUploadService {
uploadEvents: (userId?: string) => Promise<void>;
}

View File

@@ -12,6 +12,7 @@ export abstract class FolderService {
clearCache: () => Promise<void>;
encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise<Folder>;
get: (id: string) => Promise<Folder>;
getAllFromState: () => Promise<Folder[]>;
/**
* @deprecated Only use in CLI!
*/

View File

@@ -6,6 +6,6 @@ export abstract class I18nService {
translationLocale: string;
collator: Intl.Collator;
localeNames: Map<string, string>;
t: (id: string, p1?: string, p2?: string, p3?: string) => string;
t: (id: string, p1?: string | number, p2?: string | number, p3?: string | number) => string;
translate: (id: string, p1?: string, p2?: string, p3?: string) => string;
}

View File

@@ -4,4 +4,5 @@ export abstract class LoginService {
setEmail: (value: string) => void;
setRememberEmail: (value: boolean) => void;
clearValues: () => void;
saveEmailSettings: () => Promise<void>;
}

View File

@@ -0,0 +1,242 @@
import { ListResponse } from "../../models/response/list.response";
import {
OrganizationUserAcceptRequest,
OrganizationUserBulkConfirmRequest,
OrganizationUserConfirmRequest,
OrganizationUserInviteRequest,
OrganizationUserResetPasswordEnrollmentRequest,
OrganizationUserResetPasswordRequest,
OrganizationUserUpdateGroupsRequest,
OrganizationUserUpdateRequest,
} from "./requests";
import {
OrganizationUserBulkPublicKeyResponse,
OrganizationUserBulkResponse,
OrganizationUserDetailsResponse,
OrganizationUserResetPasswordDetailsReponse,
OrganizationUserUserDetailsResponse,
} from "./responses";
/**
* Service for interacting with Organization Users via the API
*/
export abstract class OrganizationUserService {
/**
* Retrieve a single organization user by Id
* @param organizationId - Identifier for the user's organization
* @param id - Organization user identifier
* @param options - Options for the request
*/
abstract getOrganizationUser(
organizationId: string,
id: string,
options?: {
includeGroups?: boolean;
}
): Promise<OrganizationUserDetailsResponse>;
/**
* Retrieve a list of groups Ids the specified organization user belongs to
* @param organizationId - Identifier for the user's organization
* @param id - Organization user identifier
*/
abstract getOrganizationUserGroups(organizationId: string, id: string): Promise<string[]>;
/**
* Retrieve a list of all users that belong to the specified organization
* @param organizationId - Identifier for the organization
* @param options - Options for the request
*/
abstract getAllUsers(
organizationId: string,
options?: {
includeCollections?: boolean;
includeGroups?: boolean;
}
): Promise<ListResponse<OrganizationUserUserDetailsResponse>>;
/**
* Retrieve reset password details for the specified organization user
* @param organizationId - Identifier for the user's organization
* @param id - Organization user identifier
*/
abstract getOrganizationUserResetPasswordDetails(
organizationId: string,
id: string
): Promise<OrganizationUserResetPasswordDetailsReponse>;
/**
* Create new organization user invite(s) for the specified organization
* @param organizationId - Identifier for the organization
* @param request - New user invitation request details
*/
abstract postOrganizationUserInvite(
organizationId: string,
request: OrganizationUserInviteRequest
): Promise<void>;
/**
* Re-invite the specified organization user
* @param organizationId - Identifier for the user's organization
* @param id - Organization user identifier
*/
abstract postOrganizationUserReinvite(organizationId: string, id: string): Promise<any>;
/**
* Re-invite many organization users for the specified organization
* @param organizationId - Identifier for the organization
* @param ids - A list of organization user identifiers
* @return List of user ids, including both those that were successfully re-invited and those that had an error
*/
abstract postManyOrganizationUserReinvite(
organizationId: string,
ids: string[]
): Promise<ListResponse<OrganizationUserBulkResponse>>;
/**
* Accept an organization user invitation
* @param organizationId - Identifier for the organization to accept
* @param id - Organization user identifier
* @param request - Request details for accepting the invitation
*/
abstract postOrganizationUserAccept(
organizationId: string,
id: string,
request: OrganizationUserAcceptRequest
): Promise<void>;
/**
* Confirm an organization user that has accepted their invitation
* @param organizationId - Identifier for the organization to confirm
* @param id - Organization user identifier
* @param request - Request details for confirming the user
*/
abstract postOrganizationUserConfirm(
organizationId: string,
id: string,
request: OrganizationUserConfirmRequest
): Promise<void>;
/**
* Retrieve a list of the specified users' public keys
* @param organizationId - Identifier for the organization to accept
* @param ids - A list of organization user identifiers to retrieve public keys for
*/
abstract postOrganizationUsersPublicKey(
organizationId: string,
ids: string[]
): Promise<ListResponse<OrganizationUserBulkPublicKeyResponse>>;
/**
* Confirm many organization users that have accepted their invitations
* @param organizationId - Identifier for the organization to confirm users
* @param request - Bulk request details for confirming the user
*/
abstract postOrganizationUserBulkConfirm(
organizationId: string,
request: OrganizationUserBulkConfirmRequest
): Promise<ListResponse<OrganizationUserBulkResponse>>;
/**
* Update an organization users
* @param organizationId - Identifier for the organization the user belongs to
* @param id - Organization user identifier
* @param request - Request details for updating the user
*/
abstract putOrganizationUser(
organizationId: string,
id: string,
request: OrganizationUserUpdateRequest
): Promise<void>;
/**
* Update an organization user's groups
* @param organizationId - Identifier for the organization the user belongs to
* @param id - Organization user identifier
* @param groupIds - List of group ids to associate the user with
*/
abstract putOrganizationUserGroups(
organizationId: string,
id: string,
groupIds: OrganizationUserUpdateGroupsRequest
): Promise<void>;
/**
* Update an organization user's reset password enrollment
* @param organizationId - Identifier for the organization the user belongs to
* @param userId - Organization user identifier
* @param request - Reset password enrollment details
*/
abstract putOrganizationUserResetPasswordEnrollment(
organizationId: string,
userId: string,
request: OrganizationUserResetPasswordEnrollmentRequest
): Promise<void>;
/**
* Reset an organization user's password
* @param organizationId - Identifier for the organization the user belongs to
* @param id - Organization user identifier
* @param request - Reset password details
*/
abstract putOrganizationUserResetPassword(
organizationId: string,
id: string,
request: OrganizationUserResetPasswordRequest
): Promise<void>;
/**
* Delete an organization user
* @param organizationId - Identifier for the organization the user belongs to
* @param id - Organization user identifier
*/
abstract deleteOrganizationUser(organizationId: string, id: string): Promise<void>;
/**
* Delete many organization users
* @param organizationId - Identifier for the organization the users belongs to
* @param ids - List of organization user identifiers to delete
* @return List of user ids, including both those that were successfully deleted and those that had an error
*/
abstract deleteManyOrganizationUsers(
organizationId: string,
ids: string[]
): Promise<ListResponse<OrganizationUserBulkResponse>>;
/**
* Revoke an organization user's access to the organization
* @param organizationId - Identifier for the organization the user belongs to
* @param id - Organization user identifier
*/
abstract revokeOrganizationUser(organizationId: string, id: string): Promise<void>;
/**
* Revoke many organization users' access to the organization
* @param organizationId - Identifier for the organization the users belongs to
* @param ids - List of organization user identifiers to revoke
* @return List of user ids, including both those that were successfully revoked and those that had an error
*/
abstract revokeManyOrganizationUsers(
organizationId: string,
ids: string[]
): Promise<ListResponse<OrganizationUserBulkResponse>>;
/**
* Restore an organization user's access to the organization
* @param organizationId - Identifier for the organization the user belongs to
* @param id - Organization user identifier
*/
abstract restoreOrganizationUser(organizationId: string, id: string): Promise<void>;
/**
* Restore many organization users' access to the organization
* @param organizationId - Identifier for the organization the users belongs to
* @param ids - List of organization user identifiers to restore
* @return List of user ids, including both those that were successfully restored and those that had an error
*/
abstract restoreManyOrganizationUsers(
organizationId: string,
ids: string[]
): Promise<ListResponse<OrganizationUserBulkResponse>>;
}

View File

@@ -0,0 +1,8 @@
export * from "./organization-user-accept.request";
export * from "./organization-user-bulk-confirm.request";
export * from "./organization-user-confirm.request";
export * from "./organization-user-invite.request";
export * from "./organization-user-reset-password.request";
export * from "./organization-user-reset-password-enrollment.request";
export * from "./organization-user-update.request";
export * from "./organization-user-update-groups.request";

View File

@@ -0,0 +1,12 @@
import { OrganizationUserType } from "../../../enums/organizationUserType";
import { PermissionsApi } from "../../../models/api/permissions.api";
import { SelectionReadOnlyRequest } from "../../../models/request/selection-read-only.request";
export class OrganizationUserInviteRequest {
emails: string[] = [];
type: OrganizationUserType;
accessAll: boolean;
collections: SelectionReadOnlyRequest[] = [];
groups: string[];
permissions: PermissionsApi;
}

View File

@@ -1,4 +1,4 @@
import { SecretVerificationRequest } from "./secret-verification.request";
import { SecretVerificationRequest } from "../../../models/request/secret-verification.request";
export class OrganizationUserResetPasswordEnrollmentRequest extends SecretVerificationRequest {
resetPasswordKey: string;

View File

@@ -0,0 +1,11 @@
import { OrganizationUserType } from "../../../enums/organizationUserType";
import { PermissionsApi } from "../../../models/api/permissions.api";
import { SelectionReadOnlyRequest } from "../../../models/request/selection-read-only.request";
export class OrganizationUserUpdateRequest {
type: OrganizationUserType;
accessAll: boolean;
collections: SelectionReadOnlyRequest[] = [];
groups: string[] = [];
permissions: PermissionsApi;
}

View File

@@ -0,0 +1,3 @@
export * from "./organization-user.response";
export * from "./organization-user-bulk.response";
export * from "./organization-user-bulk-public-key.response";

View File

@@ -1,4 +1,4 @@
import { BaseResponse } from "./base.response";
import { BaseResponse } from "../../../models/response/base.response";
export class OrganizationUserBulkPublicKeyResponse extends BaseResponse {
id: string;

View File

@@ -1,4 +1,4 @@
import { BaseResponse } from "./base.response";
import { BaseResponse } from "../../../models/response/base.response";
export class OrganizationUserBulkResponse extends BaseResponse {
id: string;

View File

@@ -1,10 +1,9 @@
import { KdfType } from "../../enums/kdfType";
import { OrganizationUserStatusType } from "../../enums/organizationUserStatusType";
import { OrganizationUserType } from "../../enums/organizationUserType";
import { PermissionsApi } from "../api/permissions.api";
import { BaseResponse } from "./base.response";
import { SelectionReadOnlyResponse } from "./selection-read-only.response";
import { KdfType } from "../../../enums/kdfType";
import { OrganizationUserStatusType } from "../../../enums/organizationUserStatusType";
import { OrganizationUserType } from "../../../enums/organizationUserType";
import { PermissionsApi } from "../../../models/api/permissions.api";
import { BaseResponse } from "../../../models/response/base.response";
import { SelectionReadOnlyResponse } from "../../../models/response/selection-read-only.response";
export class OrganizationUserResponse extends BaseResponse {
id: string;
@@ -14,6 +13,8 @@ export class OrganizationUserResponse extends BaseResponse {
accessAll: boolean;
permissions: PermissionsApi;
resetPasswordEnrolled: boolean;
collections: SelectionReadOnlyResponse[] = [];
groups: string[] = [];
constructor(response: any) {
super(response);
@@ -24,6 +25,15 @@ export class OrganizationUserResponse extends BaseResponse {
this.permissions = new PermissionsApi(this.getResponseProperty("Permissions"));
this.accessAll = this.getResponseProperty("AccessAll");
this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled");
const collections = this.getResponseProperty("Collections");
if (collections != null) {
this.collections = collections.map((c: any) => new SelectionReadOnlyResponse(c));
}
const groups = this.getResponseProperty("Groups");
if (groups != null) {
this.groups = groups;
}
}
}
@@ -43,14 +53,8 @@ export class OrganizationUserUserDetailsResponse extends OrganizationUserRespons
}
export class OrganizationUserDetailsResponse extends OrganizationUserResponse {
collections: SelectionReadOnlyResponse[] = [];
constructor(response: any) {
super(response);
const collections = this.getResponseProperty("Collections");
if (collections != null) {
this.collections = collections.map((c: any) => new SelectionReadOnlyResponse(c));
}
}
}

View File

@@ -1,11 +1,12 @@
import { map, Observable } from "rxjs";
import { Utils } from "../../misc/utils";
import { OrganizationData } from "../../models/data/organization.data";
import { Organization } from "../../models/domain/organization";
import { I18nService } from "../i18n.service";
export function canAccessVaultTab(org: Organization): boolean {
return org.isManager;
return org.canViewAssignedCollections || org.canViewAllCollections || org.canManageGroups;
}
export function canAccessSettingsTab(org: Organization): boolean {
@@ -34,19 +35,6 @@ export function canAccessBillingTab(org: Organization): boolean {
return org.canManageBilling;
}
export function canManageCollections(org: Organization): boolean {
return (
org.canCreateNewCollections ||
org.canEditAnyCollection ||
org.canDeleteAnyCollection ||
org.canViewAssignedCollections
);
}
export function canAccessManageTab(org: Organization): boolean {
return canAccessMembersTab(org) || canAccessGroupsTab(org) || canManageCollections(org);
}
export function canAccessOrgAdmin(org: Organization): boolean {
return (
canAccessMembersTab(org) ||
@@ -54,8 +42,7 @@ export function canAccessOrgAdmin(org: Organization): boolean {
canAccessReportingTab(org) ||
canAccessBillingTab(org) ||
canAccessSettingsTab(org) ||
canAccessVaultTab(org) ||
canAccessManageTab(org)
canAccessVaultTab(org)
);
}
@@ -69,6 +56,10 @@ export function canAccessAdmin(i18nService: I18nService) {
);
}
export function isNotProviderUser(org: Organization): boolean {
return !org.isProviderUser;
}
export abstract class OrganizationService {
organizations$: Observable<Organization[]>;
@@ -83,3 +74,7 @@ export abstract class OrganizationService {
canManageSponsorships: () => Promise<boolean>;
hasOrganizations: () => boolean;
}
export abstract class InternalOrganizationService extends OrganizationService {
replace: (organizations: { [id: string]: OrganizationData }) => Promise<void>;
}

View File

@@ -19,6 +19,7 @@ export abstract class PlatformUtilsService {
isViewOpen: () => Promise<boolean>;
launchUri: (uri: string, options?: any) => void;
getApplicationVersion: () => Promise<string>;
getApplicationVersionNumber: () => Promise<string>;
supportsWebAuthn: (win: Window) => boolean;
supportsDuo: () => boolean;
showToast: (
@@ -33,7 +34,8 @@ export abstract class PlatformUtilsService {
confirmText?: string,
cancelText?: string,
type?: string,
bodyIsHtml?: boolean
bodyIsHtml?: boolean,
target?: string
) => Promise<boolean>;
isDev: () => boolean;
isSelfHost: () => boolean;

View File

@@ -349,4 +349,7 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated Do not call this directly, use ConfigService
*/
setServerConfig: (value: ServerConfigData, options?: StorageOptions) => Promise<void>;
getAvatarColor: (options?: StorageOptions) => Promise<string | null | undefined>;
setAvatarColor: (value: string, options?: StorageOptions) => Promise<void>;
}

View File

@@ -7,10 +7,11 @@ export abstract class AbstractStorageService {
abstract remove(key: string, options?: StorageOptions): Promise<void>;
}
export abstract class AbstractCachedStorageService extends AbstractStorageService {
export abstract class AbstractMemoryStorageService extends AbstractStorageService {
// Used to identify the service in the session sync decorator framework
static readonly TYPE = "MemoryStorageService";
readonly type = AbstractMemoryStorageService.TYPE;
abstract get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
abstract getBypassCache<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
}
export interface MemoryStorageServiceInterface {
get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
}

View File

@@ -1,7 +1,7 @@
import { ApiService } from "../abstractions/api.service";
import { Forwarder } from "./forwarder";
import { ForwarderOptions } from "./forwarderOptions";
import { ForwarderOptions } from "./forwarder-options";
export class AnonAddyForwarder implements Forwarder {
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {

View File

@@ -1,7 +1,7 @@
import { ApiService } from "../abstractions/api.service";
import { Forwarder } from "./forwarder";
import { ForwarderOptions } from "./forwarderOptions";
import { ForwarderOptions } from "./forwarder-options";
export class DuckDuckGoForwarder implements Forwarder {
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {

View File

@@ -1,7 +1,7 @@
import { ApiService } from "../abstractions/api.service";
import { Forwarder } from "./forwarder";
import { ForwarderOptions } from "./forwarderOptions";
import { ForwarderOptions } from "./forwarder-options";
export class FastmailForwarder implements Forwarder {
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {

View File

@@ -1,7 +1,7 @@
import { ApiService } from "../abstractions/api.service";
import { Forwarder } from "./forwarder";
import { ForwarderOptions } from "./forwarderOptions";
import { ForwarderOptions } from "./forwarder-options";
export class FirefoxRelayForwarder implements Forwarder {
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {

View File

@@ -1,6 +1,6 @@
import { ApiService } from "../abstractions/api.service";
import { ForwarderOptions } from "./forwarderOptions";
import { ForwarderOptions } from "./forwarder-options";
export interface Forwarder {
generate(apiService: ApiService, options: ForwarderOptions): Promise<string>;

View File

@@ -1,7 +1,7 @@
import { ApiService } from "../abstractions/api.service";
import { Forwarder } from "./forwarder";
import { ForwarderOptions } from "./forwarderOptions";
import { ForwarderOptions } from "./forwarder-options";
export class SimpleLoginForwarder implements Forwarder {
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {

View File

@@ -67,6 +67,7 @@ export const regularImportOptions = [
{ id: "encryptrcsv", name: "Encryptr (csv)" },
{ id: "yoticsv", name: "Yoti (csv)" },
{ id: "nordpasscsv", name: "Nordpass (csv)" },
{ id: "passkyjson", name: "Passky (json)" },
] as const;
export type ImportType =

View File

@@ -3,5 +3,5 @@ export enum KdfType {
}
export const DEFAULT_KDF_TYPE = KdfType.PBKDF2_SHA256;
export const DEFAULT_KDF_ITERATIONS = 100000;
export const DEFAULT_KDF_ITERATIONS = 600000;
export const SEND_KDF_ITERATIONS = 100000;

View File

@@ -137,8 +137,7 @@ export class DashlaneCsvImporter extends BaseImporter implements Importer {
cipher.card.number = row.cc_number;
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.code = row.code;
cipher.card.expMonth = row.expiration_month;
cipher.card.expYear = row.expiration_year.substring(2, 4);
this.setCardExpiration(cipher, `${row.expiration_month}/${row.expiration_year}`);
// If you add more mapped fields please extend this
mappedValues = [

View File

@@ -1,11 +1,10 @@
import { CipherType } from "../enums/cipherType";
import { SecureNoteType } from "../enums/secureNoteType";
import { ImportResult } from "../models/domain/import-result";
import { CardView } from "../models/view/card.view";
import { SecureNoteView } from "../models/view/secure-note.view";
import { BaseImporter } from "./base-importer";
import { Importer } from "./importer";
import { CipherType } from "../../enums/cipherType";
import { SecureNoteType } from "../../enums/secureNoteType";
import { ImportResult } from "../../models/domain/import-result";
import { CardView } from "../../models/view/card.view";
import { SecureNoteView } from "../../models/view/secure-note.view";
import { BaseImporter } from "../base-importer";
import { Importer } from "../importer";
export class EnpassCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {

View File

@@ -1,17 +1,21 @@
import { CipherType } from "../enums/cipherType";
import { FieldType } from "../enums/fieldType";
import { ImportResult } from "../models/domain/import-result";
import { CardView } from "../models/view/card.view";
import { CipherView } from "../models/view/cipher.view";
import { FolderView } from "../models/view/folder.view";
import { CipherType } from "../../enums/cipherType";
import { FieldType } from "../../enums/fieldType";
import { ImportResult } from "../../models/domain/import-result";
import { CardView } from "../../models/view/card.view";
import { CipherView } from "../../models/view/cipher.view";
import { FolderView } from "../../models/view/folder.view";
import { BaseImporter } from "../base-importer";
import { Importer } from "../importer";
import { BaseImporter } from "./base-importer";
import { Importer } from "./importer";
import { EnpassJsonFile, EnpassFolder, EnpassField } from "./types/enpass-json-type";
type EnpassFolderTreeItem = EnpassFolder & { children: EnpassFolderTreeItem[] };
const androidUrlRegex = new RegExp("androidapp://.*==@", "g");
export class EnpassJsonImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = JSON.parse(data);
const results: EnpassJsonFile = JSON.parse(data);
if (results == null || results.items == null || results.items.length === 0) {
result.success = false;
return Promise.resolve(result);
@@ -28,7 +32,7 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
result.folders.push(f);
});
results.items.forEach((item: any) => {
results.items.forEach((item) => {
if (item.folders != null && item.folders.length > 0 && foldersIndexMap.has(item.folders[0])) {
result.folderRelationships.push([
result.ciphers.length,
@@ -50,7 +54,7 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
this.processCard(cipher, item.fields);
} else if (
item.template_type.indexOf("identity.") < 0 &&
item.fields.some((f: any) => f.type === "password" && !this.isNullOrWhitespace(f.value))
item.fields.some((f) => f.type === "password" && !this.isNullOrWhitespace(f.value))
) {
this.processLogin(cipher, item.fields);
} else {
@@ -68,9 +72,9 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
return Promise.resolve(result);
}
private processLogin(cipher: CipherView, fields: any[]) {
private processLogin(cipher: CipherView, fields: EnpassField[]) {
const urls: string[] = [];
fields.forEach((field: any) => {
fields.forEach((field) => {
if (this.isNullOrWhitespace(field.value) || field.type === "section") {
return;
}
@@ -86,6 +90,13 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
cipher.login.totp = field.value;
} else if (field.type === "url") {
urls.push(field.value);
} else if (field.type === ".Android#") {
let cleanedValue = field.value.startsWith("androidapp://")
? field.value
: "androidapp://" + field.value;
cleanedValue = cleanedValue.replace("android://", "");
cleanedValue = cleanedValue.replace(androidUrlRegex, "androidapp://");
urls.push(cleanedValue);
} else {
this.processKvp(
cipher,
@@ -98,10 +109,10 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
cipher.login.uris = this.makeUriArray(urls);
}
private processCard(cipher: CipherView, fields: any[]) {
private processCard(cipher: CipherView, fields: EnpassField[]) {
cipher.card = new CardView();
cipher.type = CipherType.Card;
fields.forEach((field: any) => {
fields.forEach((field) => {
if (
this.isNullOrWhitespace(field.value) ||
field.type === "section" ||
@@ -137,8 +148,8 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
});
}
private processNote(cipher: CipherView, fields: any[]) {
fields.forEach((field: any) => {
private processNote(cipher: CipherView, fields: EnpassField[]) {
fields.forEach((field) => {
if (this.isNullOrWhitespace(field.value) || field.type === "section") {
return;
}
@@ -151,17 +162,17 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
});
}
private buildFolderTree(folders: any[]): any[] {
private buildFolderTree(folders: EnpassFolder[]): EnpassFolderTreeItem[] {
if (folders == null) {
return [];
}
const folderTree: any[] = [];
const map = new Map<string, any>([]);
folders.forEach((obj: any) => {
const folderTree: EnpassFolderTreeItem[] = [];
const map = new Map<string, EnpassFolderTreeItem>([]);
folders.forEach((obj: EnpassFolderTreeItem) => {
map.set(obj.uuid, obj);
obj.children = [];
});
folders.forEach((obj: any) => {
folders.forEach((obj: EnpassFolderTreeItem) => {
if (obj.parent_uuid != null && obj.parent_uuid !== "" && map.has(obj.parent_uuid)) {
map.get(obj.parent_uuid).children.push(obj);
} else {
@@ -171,11 +182,15 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
return folderTree;
}
private flattenFolderTree(titlePrefix: string, tree: any[], map: Map<string, string>) {
private flattenFolderTree(
titlePrefix: string,
tree: EnpassFolderTreeItem[],
map: Map<string, string>
) {
if (tree == null) {
return;
}
tree.forEach((f: any) => {
tree.forEach((f) => {
if (f.title != null && f.title.trim() !== "") {
let title = f.title.trim();
if (titlePrefix != null && titlePrefix.trim() !== "") {

View File

@@ -0,0 +1,79 @@
type Login = "login.default";
type CreditCard = "creditcard.default";
type Identity = "identity.default";
type Note = "note.default";
type Password = "password.default";
type Finance =
| "finance.stock"
| "finance.bankaccount"
| "finance.loan"
| "finance.mutualfund"
| "finance.insurance"
| "finance.other";
type License = "license.driving" | "license.hunting" | "license.software" | "license.other";
type Travel =
| "travel.passport"
| "travel.flightdetails"
| "travel.hotelreservation"
| "travel.visa"
| "travel.freqflyer"
| "travel.other";
type Computer =
| "computer.database"
| "computer.emailaccount"
| "computer.ftp"
| "computer.messaging"
| "computer.internetprovider"
| "computer.server"
| "computer.wifi"
| "computer.hosting"
| "computer.other";
type Misc =
| "misc.Aadhar"
| "misc.address"
| "misc.library"
| "misc.rewardprogram"
| "misc.lens"
| "misc.service"
| "misc.vehicleinfo"
| "misc.itic"
| "misc.itz"
| "misc.propertyinfo"
| "misc.clothsize"
| "misc.contact"
| "misc.membership"
| "misc.cellphone"
| "misc.emergencyno"
| "misc.pan"
| "misc.identity"
| "misc.regcode"
| "misc.prescription"
| "misc.serial"
| "misc.socialsecurityno"
| "misc.isic"
| "misc.calling"
| "misc.voicemail"
| "misc.voter"
| "misc.combilock"
| "misc.other";
export type EnpassItemTemplate =
| Login
| CreditCard
| Identity
| Note
| Password
| Finance
| License
| Travel
| Computer
| Misc;

View File

@@ -0,0 +1,85 @@
import { EnpassItemTemplate } from "./enpass-item-templates";
export type EnpassJsonFile = {
folders: EnpassFolder[];
items: EnpassItem[];
};
export type EnpassFolder = {
icon: string;
parent_uuid: string;
title: string;
updated_at: number;
uuid: string;
};
export type EnpassItem = {
archived: number;
auto_submit: number;
category: string;
createdAt: number;
favorite: number;
fields?: EnpassField[];
icon: Icon;
note: string;
subtitle: string;
template_type: EnpassItemTemplate;
title: string;
trashed: number;
updated_at: number;
uuid: string;
folders?: string[];
};
export type EnpassFieldType =
| "text"
| "password"
| "pin"
| "numeric"
| "date"
| "email"
| "url"
| "phone"
| "username"
| "totp"
| "multiline"
| "ccName"
| "ccNumber"
| "ccCvc"
| "ccPin"
| "ccExpiry"
| "ccBankname"
| "ccTxnpassword"
| "ccType"
| "ccValidfrom"
| "section"
| ".Android#";
export type EnpassField = {
deleted: number;
history?: History[];
label: string;
order: number;
sensitive: number;
type: EnpassFieldType;
uid: number;
updated_at: number;
value: string;
value_updated_at: number;
};
export type History = {
updated_at: number;
value: string;
};
export type Icon = {
fav: string;
image: Image;
type: number;
uuid: string;
};
export type Image = {
file: string;
};

View File

@@ -1,59 +0,0 @@
import { CipherType } from "../enums/cipherType";
import { ImportResult } from "../models/domain/import-result";
import { CardView } from "../models/view/card.view";
import { BaseImporter } from "./base-importer";
import { Importer } from "./importer";
export class FSecureFskImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = JSON.parse(data);
if (results == null || results.data == null) {
result.success = false;
return Promise.resolve(result);
}
for (const key in results.data) {
// eslint-disable-next-line
if (!results.data.hasOwnProperty(key)) {
continue;
}
const value = results.data[key];
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(value.service);
cipher.notes = this.getValueOrDefault(value.notes);
if (value.style === "website" || value.style === "globe") {
cipher.login.username = this.getValueOrDefault(value.username);
cipher.login.password = this.getValueOrDefault(value.password);
cipher.login.uris = this.makeUriArray(value.url);
} else if (value.style === "creditcard") {
cipher.type = CipherType.Card;
cipher.card = new CardView();
cipher.card.cardholderName = this.getValueOrDefault(value.username);
cipher.card.number = this.getValueOrDefault(value.creditNumber);
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.code = this.getValueOrDefault(value.creditCvv);
if (!this.isNullOrWhitespace(value.creditExpiry)) {
if (!this.setCardExpiration(cipher, value.creditExpiry)) {
this.processKvp(cipher, "Expiration", value.creditExpiry);
}
}
if (!this.isNullOrWhitespace(value.password)) {
this.processKvp(cipher, "PIN", value.password);
}
} else {
continue;
}
this.convertToNoteIfNeeded(cipher);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
}
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -0,0 +1,52 @@
import { CipherType } from "../../enums/cipherType";
import { FSecureFskImporter as Importer } from "./fsecure-fsk-importer";
import { CreditCardTestEntry, LoginTestEntry } from "./fsk-test-data";
describe("FSecure FSK Importer", () => {
it("should import data of type login", async () => {
const importer = new Importer();
const LoginTestEntryStringified = JSON.stringify(LoginTestEntry);
const result = await importer.parse(LoginTestEntryStringified);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.name).toEqual("example.com");
expect(cipher.favorite).toBe(true);
expect(cipher.notes).toEqual("some note for example.com");
expect(cipher.type).toBe(CipherType.Login);
expect(cipher.login.username).toEqual("jdoe");
expect(cipher.login.password).toEqual("somePassword");
expect(cipher.login.uris.length).toEqual(1);
const uriView = cipher.login.uris.shift();
expect(uriView.uri).toEqual("https://www.example.com");
});
it("should import data of type creditCard", async () => {
const importer = new Importer();
const CreditCardTestEntryStringified = JSON.stringify(CreditCardTestEntry);
const result = await importer.parse(CreditCardTestEntryStringified);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.name).toEqual("My credit card");
expect(cipher.favorite).toBe(false);
expect(cipher.notes).toEqual("some notes to my card");
expect(cipher.type).toBe(CipherType.Card);
expect(cipher.card.cardholderName).toEqual("John Doe");
expect(cipher.card.number).toEqual("4242424242424242");
expect(cipher.card.code).toEqual("123");
expect(cipher.fields.length).toBe(2);
expect(cipher.fields[0].name).toEqual("Expiration");
expect(cipher.fields[0].value).toEqual("22.10.2026");
expect(cipher.fields[1].name).toEqual("PIN");
expect(cipher.fields[1].value).toEqual("1234");
});
});

View File

@@ -0,0 +1,79 @@
import { CipherType } from "../../enums/cipherType";
import { ImportResult } from "../../models/domain/import-result";
import { CardView } from "../../models/view/card.view";
import { CipherView } from "../../models/view/cipher.view";
import { BaseImporter } from "../base-importer";
import { Importer } from "../importer";
import { FskEntry, FskEntryTypesEnum, FskFile } from "./fsecure-fsk-types";
export class FSecureFskImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results: FskFile = JSON.parse(data);
if (results == null || results.data == null) {
result.success = false;
return Promise.resolve(result);
}
for (const key in results.data) {
// eslint-disable-next-line
if (!results.data.hasOwnProperty(key)) {
continue;
}
const value = results.data[key];
const cipher = this.parseEntry(value);
result.ciphers.push(cipher);
}
result.success = true;
return Promise.resolve(result);
}
private parseEntry(entry: FskEntry): CipherView {
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(entry.service);
cipher.notes = this.getValueOrDefault(entry.notes);
cipher.favorite = entry.favorite > 0;
switch (entry.type) {
case FskEntryTypesEnum.Login:
this.handleLoginEntry(entry, cipher);
break;
case FskEntryTypesEnum.CreditCard:
this.handleCreditCardEntry(entry, cipher);
break;
default:
return;
break;
}
this.convertToNoteIfNeeded(cipher);
this.cleanupCipher(cipher);
return cipher;
}
private handleLoginEntry(entry: FskEntry, cipher: CipherView) {
cipher.login.username = this.getValueOrDefault(entry.username);
cipher.login.password = this.getValueOrDefault(entry.password);
cipher.login.uris = this.makeUriArray(entry.url);
}
private handleCreditCardEntry(entry: FskEntry, cipher: CipherView) {
cipher.type = CipherType.Card;
cipher.card = new CardView();
cipher.card.cardholderName = this.getValueOrDefault(entry.username);
cipher.card.number = this.getValueOrDefault(entry.creditNumber);
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.code = this.getValueOrDefault(entry.creditCvv);
if (!this.isNullOrWhitespace(entry.creditExpiry)) {
if (!this.setCardExpiration(cipher, entry.creditExpiry)) {
this.processKvp(cipher, "Expiration", entry.creditExpiry);
}
}
if (!this.isNullOrWhitespace(entry.password)) {
this.processKvp(cipher, "PIN", entry.password);
}
}
}

View File

@@ -0,0 +1,37 @@
export interface FskFile {
data: Data;
}
export interface Data {
[key: string]: FskEntry;
}
export enum FskEntryTypesEnum {
Login = 1,
CreditCard = 2,
}
export interface FskEntry {
color: string;
creditCvv: string;
creditExpiry: string;
creditNumber: string;
favorite: number; // UNIX timestamp
notes: string;
password: string;
passwordList: PasswordList[];
passwordModifiedDate: number; // UNIX timestamp
rev: string | number;
service: string;
style: string;
type: FskEntryTypesEnum;
url: string;
username: string;
createdDate: number; // UNIX timestamp
modifiedDate: number; // UNIX timestamp
}
export interface PasswordList {
changedate: string;
password: string;
}

View File

@@ -0,0 +1,49 @@
import { FskFile } from "./fsecure-fsk-types";
export const LoginTestEntry: FskFile = {
data: {
"1c3a2e31dcaa8459edd70a9d895ce298": {
color: "#00A34D",
createdDate: 0,
creditCvv: "",
creditExpiry: "",
creditNumber: "",
favorite: 1666440874,
modifiedDate: 0,
notes: "some note for example.com",
password: "somePassword",
passwordList: [],
passwordModifiedDate: 0,
rev: 1,
service: "example.com",
style: "website",
type: 1,
url: "https://www.example.com",
username: "jdoe",
},
},
};
export const CreditCardTestEntry: FskFile = {
data: {
"156498a46a3254f16035cbbbd09c2b8f": {
color: "#00baff",
createdDate: 1666438977,
creditCvv: "123",
creditExpiry: "22.10.2026",
creditNumber: "4242424242424242",
favorite: 0,
modifiedDate: 1666438977,
notes: "some notes to my card",
password: "1234",
passwordList: [],
passwordModifiedDate: 1666438977,
rev: 1,
service: "My credit card",
style: "creditcard",
type: 2,
url: "mybank",
username: "John Doe",
},
},
};

View File

@@ -15,7 +15,23 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer {
return Promise.resolve(this.result);
}
const rootGroup = doc.querySelector("KeePassFile > Root > Group");
//Note: The doc.querySelector("KeePassFile > Root > Group") no longers works on node and we have to breakdown the query by nodes
const KeePassFileNode = doc.querySelector("KeePassFile");
if (KeePassFileNode == null) {
this.result.errorMessage = "Missing `KeePassFile` node.";
this.result.success = false;
return Promise.resolve(this.result);
}
const RootNode = KeePassFileNode.querySelector("Root");
if (RootNode == null) {
this.result.errorMessage = "Missing `KeePassFile > Root` node.";
this.result.success = false;
return Promise.resolve(this.result);
}
const rootGroup = RootNode.querySelector("Group");
if (rootGroup == null) {
this.result.errorMessage = "Missing `KeePassFile > Root > Group` node.";
this.result.success = false;

View File

@@ -18,7 +18,12 @@ export class KeeperCsvImporter extends BaseImporter implements Importer {
this.processFolder(result, value[0]);
const cipher = this.initLoginCipher();
cipher.notes = this.getValueOrDefault(value[5]) + "\n";
const notes = this.getValueOrDefault(value[5]);
if (notes) {
cipher.notes = `${notes}\n`;
}
cipher.name = this.getValueOrDefault(value[1], "--");
cipher.login.username = this.getValueOrDefault(value[2]);
cipher.login.password = this.getValueOrDefault(value[3]);
@@ -27,7 +32,11 @@ export class KeeperCsvImporter extends BaseImporter implements Importer {
if (value.length > 7) {
// we have some custom fields.
for (let i = 7; i < value.length; i = i + 2) {
this.processKvp(cipher, value[i], value[i + 1]);
if (value[i] == "TFC:Keeper") {
cipher.login.totp = value[i + 1];
} else {
this.processKvp(cipher, value[i], value[i + 1]);
}
}
}

View File

@@ -0,0 +1,43 @@
import { ImportResult } from "../../models/domain/import-result";
import { BaseImporter } from "../base-importer";
import { Importer } from "../importer";
import { PasskyJsonExport } from "./passky-json-types";
export class PasskyJsonImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const passkyExport: PasskyJsonExport = JSON.parse(data);
if (
passkyExport == null ||
passkyExport.passwords == null ||
passkyExport.passwords.length === 0
) {
result.success = false;
return Promise.resolve(result);
}
if (passkyExport.encrypted == true) {
result.success = false;
result.errorMessage = "Unable to import an encrypted passky backup.";
return Promise.resolve(result);
}
passkyExport.passwords.forEach((record) => {
const cipher = this.initLoginCipher();
cipher.name = record.website;
cipher.login.username = record.username;
cipher.login.password = record.password;
cipher.login.uris = this.makeUriArray(record.website);
cipher.notes = record.message;
this.convertToNoteIfNeeded(cipher);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

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