1
0
mirror of https://github.com/bitwarden/jslib synced 2025-12-06 00:03:29 +00:00

Merge branch 'rc' of github.com:bitwarden/jslib into rc

This commit is contained in:
Kyle Spearrin
2022-05-13 10:41:09 -04:00
184 changed files with 24075 additions and 8594 deletions

View File

@@ -17,10 +17,6 @@
- **file.ext:** Description of what was changed and why
## Testing requirements
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
## Before you submit
- [ ] I have checked for **linting** errors (`npm run lint`) (required)

View File

@@ -1,5 +1,11 @@
[![Github Workflow build on master](https://github.com/bitwarden/jslib/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/bitwarden/jslib/actions/workflows/build.yml?query=branch:master)
> **Repository Reorganization in Progress**
>
> We are currently migrating some projects over to a mono repository. For existing PR's we will be providing documentation on how to move/migrate them. To minimize the overhead we are actively reviewing open PRs. If possible please ensure any pending comments are resolved as soon as possible.
>
> New pull requests created during this transition period may not get addressed —if needed, please create a new PR after the reorganization is complete.
# Bitwarden JavaScript Library
Common code referenced across Bitwarden JavaScript projects.

View File

@@ -1,4 +1,4 @@
const { pathsToModuleNameMapper } = require("ts-jest/utils");
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
@@ -6,7 +6,6 @@ module.exports = {
name: "angular",
displayName: "angular tests",
preset: "jest-preset-angular",
roots: ["<rootDir>/spec/"],
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
collectCoverage: true,

View File

@@ -32,7 +32,6 @@ export class CiphersComponent {
async reload(filter: (cipher: CipherView) => boolean = null, deleted = false) {
this.loaded = false;
this.ciphers = [];
await this.load(filter, deleted);
}

View File

@@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
@@ -15,6 +16,7 @@ export class GeneratorComponent implements OnInit {
@Input() type: string;
@Output() onSelected = new EventEmitter<string>();
usernameGeneratingPromise: Promise<string>;
typeOptions: any[];
passTypeOptions: any[];
usernameTypeOptions: any[];
@@ -36,6 +38,7 @@ export class GeneratorComponent implements OnInit {
protected platformUtilsService: PlatformUtilsService,
protected stateService: StateService,
protected i18nService: I18nService,
protected logService: LogService,
protected route: ActivatedRoute,
private win: Window
) {
@@ -58,13 +61,20 @@ export class GeneratorComponent implements OnInit {
value: "catchall",
desc: i18nService.t("catchallEmailDesc"),
},
{
name: i18nService.t("forwardedEmail"),
value: "forwarded",
desc: i18nService.t("forwardedEmailDesc"),
},
{ name: i18nService.t("randomWord"), value: "word" },
];
this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }];
this.forwardOptions = [
{ name: "SimpleLogin", value: "simplelogin" },
{ name: "FastMail", value: "fastmail" },
{ name: "AnonAddy", value: "anonaddy" },
{ name: "Firefox Relay", value: "firefoxrelay" },
// { name: "FastMail", value: "fastmail" },
];
}
@@ -104,13 +114,17 @@ export class GeneratorComponent implements OnInit {
this.type = generatorOptions?.type ?? "password";
}
}
await this.regenerate();
if (this.regenerateWithoutButtonPress()) {
await this.regenerate();
}
});
}
async typeChanged() {
await this.stateService.setGeneratorOptions({ type: this.type });
await this.regenerate();
if (this.regenerateWithoutButtonPress()) {
await this.regenerate();
}
}
async regenerate() {
@@ -135,14 +149,17 @@ export class GeneratorComponent implements OnInit {
this.normalizePasswordOptions();
await this.passwordGenerationService.saveOptions(this.passwordOptions);
if (regenerate) {
if (regenerate && this.regenerateWithoutButtonPress()) {
await this.regeneratePassword();
}
}
async saveUsernameOptions(regenerate = true) {
await this.usernameGenerationService.saveOptions(this.usernameOptions);
if (regenerate) {
if (this.usernameOptions.type === "forwarded") {
this.username = "-";
}
if (regenerate && this.regenerateWithoutButtonPress()) {
await this.regenerateUsername();
}
}
@@ -157,9 +174,16 @@ export class GeneratorComponent implements OnInit {
}
async generateUsername() {
this.username = await this.usernameGenerationService.generateUsername(this.usernameOptions);
if (this.username === "" || this.username === null) {
this.username = "-";
try {
this.usernameGeneratingPromise = this.usernameGenerationService.generateUsername(
this.usernameOptions
);
this.username = await this.usernameGeneratingPromise;
if (this.username === "" || this.username === null) {
this.username = "-";
}
} catch (e) {
this.logService.error(e);
}
}
@@ -185,6 +209,10 @@ export class GeneratorComponent implements OnInit {
this.showOptions = !this.showOptions;
}
regenerateWithoutButtonPress() {
return this.type !== "username" || this.usernameOptions.type !== "forwarded";
}
private normalizePasswordOptions() {
// Application level normalize options depedent on class variables
this.passwordOptions.ambiguous = !this.avoidAmbiguous;

View File

@@ -1,157 +0,0 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { CipherType } from "jslib-common/enums/cipherType";
import { TreeNode } from "jslib-common/models/domain/treeNode";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from "jslib-common/models/view/folderView";
@Directive()
export class GroupingsComponent {
@Input() showFolders = true;
@Input() showCollections = true;
@Input() showFavorites = true;
@Input() showTrash = true;
@Output() onAllClicked = new EventEmitter();
@Output() onFavoritesClicked = new EventEmitter();
@Output() onTrashClicked = new EventEmitter();
@Output() onCipherTypeClicked = new EventEmitter<CipherType>();
@Output() onFolderClicked = new EventEmitter<FolderView>();
@Output() onAddFolder = new EventEmitter();
@Output() onEditFolder = new EventEmitter<FolderView>();
@Output() onCollectionClicked = new EventEmitter<CollectionView>();
folders: FolderView[];
nestedFolders: TreeNode<FolderView>[];
collections: CollectionView[];
nestedCollections: TreeNode<CollectionView>[];
loaded = false;
cipherType = CipherType;
selectedAll = false;
selectedFavorites = false;
selectedTrash = false;
selectedType: CipherType = null;
selectedFolder = false;
selectedFolderId: string = null;
selectedCollectionId: string = null;
private collapsedGroupings: Set<string>;
constructor(
protected collectionService: CollectionService,
protected folderService: FolderService,
protected stateService: StateService
) {}
async load(setLoaded = true) {
const collapsedGroupings = await this.stateService.getCollapsedGroupings();
if (collapsedGroupings == null) {
this.collapsedGroupings = new Set<string>();
} else {
this.collapsedGroupings = new Set(collapsedGroupings);
}
await this.loadFolders();
await this.loadCollections();
if (setLoaded) {
this.loaded = true;
}
}
async loadCollections(organizationId?: string) {
if (!this.showCollections) {
return;
}
const collections = await this.collectionService.getAllDecrypted();
if (organizationId != null) {
this.collections = collections.filter((c) => c.organizationId === organizationId);
} else {
this.collections = collections;
}
this.nestedCollections = await this.collectionService.getAllNested(this.collections);
}
async loadFolders() {
if (!this.showFolders) {
return;
}
this.folders = await this.folderService.getAllDecrypted();
this.nestedFolders = await this.folderService.getAllNested();
}
selectAll() {
this.clearSelections();
this.selectedAll = true;
this.onAllClicked.emit();
}
selectFavorites() {
this.clearSelections();
this.selectedFavorites = true;
this.onFavoritesClicked.emit();
}
selectTrash() {
this.clearSelections();
this.selectedTrash = true;
this.onTrashClicked.emit();
}
selectType(type: CipherType) {
this.clearSelections();
this.selectedType = type;
this.onCipherTypeClicked.emit(type);
}
selectFolder(folder: FolderView) {
this.clearSelections();
this.selectedFolder = true;
this.selectedFolderId = folder.id;
this.onFolderClicked.emit(folder);
}
addFolder() {
this.onAddFolder.emit();
}
editFolder(folder: FolderView) {
this.onEditFolder.emit(folder);
}
selectCollection(collection: CollectionView) {
this.clearSelections();
this.selectedCollectionId = collection.id;
this.onCollectionClicked.emit(collection);
}
clearSelections() {
this.selectedAll = false;
this.selectedFavorites = false;
this.selectedTrash = false;
this.selectedType = null;
this.selectedFolder = false;
this.selectedFolderId = null;
this.selectedCollectionId = null;
}
async collapse(grouping: FolderView | CollectionView, idPrefix = "") {
if (grouping.id == null) {
return;
}
const id = idPrefix + grouping.id;
if (this.isCollapsed(grouping, idPrefix)) {
this.collapsedGroupings.delete(id);
} else {
this.collapsedGroupings.add(id);
}
await this.stateService.setCollapsedGroupings(Array.from(this.collapsedGroupings));
}
isCollapsed(grouping: FolderView | CollectionView, idPrefix = "") {
return this.collapsedGroupings.has(idPrefix + grouping.id);
}
}

View File

@@ -6,6 +6,10 @@ import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.se
import { ModalRef } from "./modal/modal.ref";
/**
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
* See UserVerificationComponent for any other situation where you need to verify the user's identity.
*/
@Directive()
export class PasswordRepromptComponent {
showPassword = false;

View File

@@ -4,6 +4,7 @@ import * as DuoWebSDK from "duo_web_sdk";
import { first } from "rxjs/operators";
import { ApiService } from "jslib-common/abstractions/api.service";
import { AppIdService } from "jslib-common/abstractions/appId.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
@@ -57,7 +58,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
protected stateService: StateService,
protected route: ActivatedRoute,
protected logService: LogService,
protected twoFactorService: TwoFactorService
protected twoFactorService: TwoFactorService,
protected appIdService: AppIdService
) {
super(environmentService, i18nService, platformUtilsService);
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
@@ -234,6 +236,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
const request = new TwoFactorEmailRequest();
request.email = this.authService.email;
request.masterPasswordHash = this.authService.masterPasswordHash;
request.deviceIdentifier = await this.appIdService.getAppId();
this.emailPromise = this.apiService.postTwoFactorEmail(request);
await this.emailPromise;
if (doToast) {

View File

@@ -61,7 +61,6 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
async cancel() {
await this.stateService.setOrganizationInvitation(null);
await this.stateService.setLoginRedirect(null);
this.router.navigate(["/vault"]);
}

View File

@@ -7,14 +7,20 @@ import { UserVerificationService } from "jslib-common/abstractions/userVerificat
import { VerificationType } from "jslib-common/enums/verificationType";
import { Verification } from "jslib-common/types/verification";
/**
* Used for general-purpose user verification throughout the app.
* Collects the user's master password, or if they are using Key Connector, prompts for an OTP via email.
* 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({
selector: "app-verify-master-password",
templateUrl: "verify-master-password.component.html",
selector: "app-user-verification",
templateUrl: "user-verification.component.html",
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: VerifyMasterPasswordComponent,
useExisting: UserVerificationComponent,
},
],
animations: [
@@ -23,7 +29,7 @@ import { Verification } from "jslib-common/types/verification";
]),
],
})
export class VerifyMasterPasswordComponent implements ControlValueAccessor, OnInit {
export class UserVerificationComponent implements ControlValueAccessor, OnInit {
usesKeyConnector = false;
disableRequestOTP = false;
sentCode = false;
@@ -41,7 +47,7 @@ export class VerifyMasterPasswordComponent implements ControlValueAccessor, OnIn
this.usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
this.processChanges(this.secret.value);
this.secret.valueChanges.subscribe((secret) => this.processChanges(secret));
this.secret.valueChanges.subscribe((secret: string) => this.processChanges(secret));
}
async requestOTP() {

View File

@@ -5,6 +5,12 @@ import { ErrorResponse } from "jslib-common/models/response/errorResponse";
import { ValidationService } from "../services/validation.service";
/**
* Provides error handling, in particular for any error returned by the server in an api call.
* Attach it to a <form> element and provide the name of the class property that will hold the api call promise.
* e.g. <form [appApiAction]="this.formPromise">
* Any errors/rejections that occur will be intercepted and displayed as error toasts.
*/
@Directive({
selector: "[appApiAction]",
})

View File

@@ -1,43 +1,40 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
@Injectable()
export class AuthGuardService implements CanActivate {
export class AuthGuard implements CanActivate {
constructor(
private vaultTimeoutService: VaultTimeoutService,
private authService: AuthService,
private router: Router,
private messagingService: MessagingService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService
private keyConnectorService: KeyConnectorService
) {}
async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) {
const isAuthed = await this.stateService.getIsAuthenticated();
if (!isAuthed) {
this.messagingService.send("authBlocked");
const authStatus = await this.authService.getAuthStatus();
if (authStatus === AuthenticationStatus.LoggedOut) {
this.messagingService.send("authBlocked", { url: routerState.url });
return false;
}
const locked = await this.vaultTimeoutService.isLocked();
if (locked) {
if (authStatus === AuthenticationStatus.Locked) {
if (routerState != null) {
this.messagingService.send("lockedUrl", { url: routerState.url });
}
this.router.navigate(["lock"], { queryParams: { promptBiometric: true } });
return false;
return this.router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } });
}
if (
!routerState.url.includes("remove-password") &&
(await this.keyConnectorService.getConvertAccountRequired())
) {
this.router.navigate(["/remove-password"]);
return false;
return this.router.createUrlTree(["/remove-password"]);
}
return true;

View File

@@ -0,0 +1,25 @@
import { Injectable } from "@angular/core";
import { CanActivate, Router } from "@angular/router";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
@Injectable()
export class LockGuard implements CanActivate {
protected homepage = "vault";
protected loginpage = "login";
constructor(private authService: AuthService, private router: Router) {}
async canActivate() {
const authStatus = await this.authService.getAuthStatus();
if (authStatus === AuthenticationStatus.Locked) {
return true;
}
const redirectUrl =
authStatus === AuthenticationStatus.LoggedOut ? [this.loginpage] : [this.homepage];
return this.router.createUrlTree([redirectUrl]);
}
}

View File

@@ -0,0 +1,25 @@
import { Injectable } from "@angular/core";
import { CanActivate, Router } from "@angular/router";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
@Injectable()
export class UnauthGuard implements CanActivate {
protected homepage = "vault";
constructor(private authService: AuthService, private router: Router) {}
async canActivate() {
const authStatus = await this.authService.getAuthStatus();
if (authStatus === AuthenticationStatus.LoggedOut) {
return true;
}
if (authStatus === AuthenticationStatus.Locked) {
return this.router.createUrlTree(["lock"]);
}
return this.router.createUrlTree([this.homepage]);
}
}

View File

@@ -23,6 +23,7 @@ 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";
@@ -44,15 +45,19 @@ import { UserNamePipe } from "./pipes/user-name.pipe";
A11yInvalidDirective,
A11yTitleDirective,
ApiActionDirective,
AvatarComponent,
AutofocusDirective,
AvatarComponent,
BlurClickDirective,
BoxRowDirective,
CalloutComponent,
ColorPasswordCountPipe,
ColorPasswordPipe,
CreditCardNumberPipe,
EllipsisPipe,
ExportScopeCalloutComponent,
FallbackSrcDirective,
I18nPipe,
IconComponent,
InputStripSpacesDirective,
InputVerbatimDirective,
NotPremiumDirective,
@@ -63,24 +68,25 @@ import { UserNamePipe } from "./pipes/user-name.pipe";
StopPropDirective,
TrueFalseValueDirective,
UserNamePipe,
CalloutComponent,
IconComponent,
ExportScopeCalloutComponent,
],
exports: [
A11yInvalidDirective,
A11yTitleDirective,
ApiActionDirective,
AvatarComponent,
AutofocusDirective,
AvatarComponent,
BitwardenToastModule,
BlurClickDirective,
BoxRowDirective,
CalloutComponent,
ColorPasswordCountPipe,
ColorPasswordPipe,
CreditCardNumberPipe,
EllipsisPipe,
ExportScopeCalloutComponent,
FallbackSrcDirective,
I18nPipe,
IconComponent,
InputStripSpacesDirective,
InputVerbatimDirective,
NotPremiumDirective,
@@ -91,10 +97,7 @@ import { UserNamePipe } from "./pipes/user-name.pipe";
StopPropDirective,
TrueFalseValueDirective,
UserNamePipe,
CalloutComponent,
IconComponent,
ExportScopeCalloutComponent,
],
providers: [UserNamePipe, SearchPipe, I18nPipe, DatePipe],
providers: [CreditCardNumberPipe, DatePipe, I18nPipe, SearchPipe, UserNamePipe],
})
export class JslibModule {}

View File

@@ -0,0 +1,51 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
import { ITreeNodeObject } from "jslib-common/models/domain/treeNode";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
import { TopLevelTreeNode } from "../models/top-level-tree-node.model";
import { VaultFilter } from "../models/vault-filter.model";
@Directive()
export class CollectionFilterComponent {
@Input() hide = false;
@Input() collapsedFilterNodes: Set<string>;
@Input() collectionNodes: DynamicTreeNode<CollectionView>;
@Input() activeFilter: VaultFilter;
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
new EventEmitter<ITreeNodeObject>();
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
readonly collectionsGrouping: TopLevelTreeNode = {
id: "collections",
name: "collections",
};
get collections() {
return this.collectionNodes?.fullList;
}
get nestedCollections() {
return this.collectionNodes?.nestedList;
}
get show() {
return !this.hide && this.collections != null && this.collections.length > 0;
}
isCollapsed(node: ITreeNodeObject) {
return this.collapsedFilterNodes.has(node.id);
}
applyFilter(collection: CollectionView) {
this.activeFilter.resetFilter();
this.activeFilter.selectedCollectionId = collection.id;
this.onFilterChange.emit(this.activeFilter);
}
async toggleCollapse(node: ITreeNodeObject) {
this.onNodeCollapseStateChange.emit(node);
}
}

View File

@@ -0,0 +1,58 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
import { ITreeNodeObject } from "jslib-common/models/domain/treeNode";
import { FolderView } from "jslib-common/models/view/folderView";
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
import { TopLevelTreeNode } from "../models/top-level-tree-node.model";
import { VaultFilter } from "../models/vault-filter.model";
@Directive()
export class FolderFilterComponent {
@Input() hide = false;
@Input() collapsedFilterNodes: Set<string>;
@Input() folderNodes: DynamicTreeNode<FolderView>;
@Input() activeFilter: VaultFilter;
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
new EventEmitter<ITreeNodeObject>();
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
@Output() onAddFolder = new EventEmitter();
@Output() onEditFolder = new EventEmitter<FolderView>();
get folders() {
return this.folderNodes?.fullList;
}
get nestedFolders() {
return this.folderNodes?.nestedList;
}
readonly foldersGrouping: TopLevelTreeNode = {
id: "folders",
name: "folders",
};
applyFilter(folder: FolderView) {
this.activeFilter.resetFilter();
this.activeFilter.selectedFolder = true;
this.activeFilter.selectedFolderId = folder.id;
this.onFilterChange.emit(this.activeFilter);
}
addFolder() {
this.onAddFolder.emit();
}
editFolder(folder: FolderView) {
this.onEditFolder.emit(folder);
}
isCollapsed(node: ITreeNodeObject) {
return this.collapsedFilterNodes.has(node.id);
}
async toggleCollapse(node: ITreeNodeObject) {
this.onNodeCollapseStateChange.emit(node);
}
}

View File

@@ -0,0 +1,78 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
import { Organization } from "jslib-common/models/domain/organization";
import { ITreeNodeObject } from "jslib-common/models/domain/treeNode";
import { DisplayMode } from "../models/display-mode";
import { TopLevelTreeNode } from "../models/top-level-tree-node.model";
import { VaultFilter } from "../models/vault-filter.model";
@Directive()
export class OrganizationFilterComponent {
@Input() hide = false;
@Input() collapsedFilterNodes: Set<string>;
@Input() organizations: Organization[];
@Input() activeFilter: VaultFilter;
@Input() activePersonalOwnershipPolicy: boolean;
@Input() activeSingleOrganizationPolicy: boolean;
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
new EventEmitter<ITreeNodeObject>();
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
get displayMode(): DisplayMode {
let displayMode: DisplayMode = "organizationMember";
if (this.organizations == null || this.organizations.length < 1) {
displayMode = "noOrganizations";
} else if (this.activePersonalOwnershipPolicy && !this.activeSingleOrganizationPolicy) {
displayMode = "personalOwnershipPolicy";
} else if (!this.activePersonalOwnershipPolicy && this.activeSingleOrganizationPolicy) {
displayMode = "singleOrganizationPolicy";
} else if (this.activePersonalOwnershipPolicy && this.activeSingleOrganizationPolicy) {
displayMode = "singleOrganizationAndPersonalOwnershipPolicies";
}
return displayMode;
}
get hasActiveFilter() {
return this.activeFilter.myVaultOnly || this.activeFilter.selectedOrganizationId != null;
}
readonly organizationGrouping: TopLevelTreeNode = {
id: "vaults",
name: "allVaults",
};
async applyOrganizationFilter(organization: Organization) {
this.activeFilter.selectedOrganizationId = organization.id;
this.activeFilter.myVaultOnly = false;
this.activeFilter.refreshCollectionsAndFolders = true;
this.applyFilter(this.activeFilter);
}
async applyMyVaultFilter() {
this.activeFilter.selectedOrganizationId = null;
this.activeFilter.myVaultOnly = true;
this.activeFilter.refreshCollectionsAndFolders = true;
this.applyFilter(this.activeFilter);
}
clearFilter() {
this.activeFilter.myVaultOnly = false;
this.activeFilter.selectedOrganizationId = null;
this.applyFilter(new VaultFilter(this.activeFilter));
}
private applyFilter(filter: VaultFilter) {
this.onFilterChange.emit(filter);
}
async toggleCollapse() {
this.onNodeCollapseStateChange.emit(this.organizationGrouping);
}
get isCollapsed() {
return this.collapsedFilterNodes.has(this.organizationGrouping.id);
}
}

View File

@@ -0,0 +1,22 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
import { CipherStatus } from "../models/cipher-status.model";
import { VaultFilter } from "../models/vault-filter.model";
@Directive()
export class StatusFilterComponent {
@Input() hideFavorites = false;
@Input() hideTrash = false;
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
@Input() activeFilter: VaultFilter;
get show() {
return !(this.hideFavorites && this.hideTrash);
}
applyFilter(cipherStatus: CipherStatus) {
this.activeFilter.resetFilter();
this.activeFilter.status = cipherStatus;
this.onFilterChange.emit(this.activeFilter);
}
}

View File

@@ -0,0 +1,40 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
import { CipherType } from "jslib-common/enums/cipherType";
import { ITreeNodeObject } from "jslib-common/models/domain/treeNode";
import { TopLevelTreeNode } from "../models/top-level-tree-node.model";
import { VaultFilter } from "../models/vault-filter.model";
@Directive()
export class TypeFilterComponent {
@Input() hide = false;
@Input() collapsedFilterNodes: Set<string>;
@Input() selectedCipherType: CipherType = null;
@Input() activeFilter: VaultFilter;
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
new EventEmitter<ITreeNodeObject>();
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
readonly typesNode: TopLevelTreeNode = {
id: "types",
name: "types",
};
cipherTypeEnum = CipherType; // used in the template
get isCollapsed() {
return this.collapsedFilterNodes.has(this.typesNode.id);
}
applyFilter(cipherType: CipherType) {
this.activeFilter.resetFilter();
this.activeFilter.cipherType = cipherType;
this.onFilterChange.emit(this.activeFilter);
}
async toggleCollapse() {
this.onNodeCollapseStateChange.emit(this.typesNode);
}
}

View File

@@ -0,0 +1 @@
export type CipherStatus = "all" | "favorites" | "trash";

View File

@@ -0,0 +1,6 @@
export type DisplayMode =
| "noOrganizations"
| "organizationMember"
| "singleOrganizationPolicy"
| "personalOwnershipPolicy"
| "singleOrganizationAndPersonalOwnershipPolicies";

View File

@@ -0,0 +1,16 @@
import { TreeNode } from "jslib-common/models/domain/treeNode";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from "jslib-common/models/view/folderView";
export class DynamicTreeNode<T extends CollectionView | FolderView> {
fullList: T[];
nestedList: TreeNode<T>[];
hasId(id: string): boolean {
return this.fullList != null && this.fullList.filter((i: T) => i.id === id).length > 0;
}
constructor(init?: Partial<DynamicTreeNode<T>>) {
Object.assign(this, init);
}
}

View File

@@ -0,0 +1,7 @@
import { ITreeNodeObject } from "jslib-common/models/domain/treeNode";
export type TopLevelTreeNodeId = "vaults" | "types" | "collections" | "folders";
export class TopLevelTreeNode implements ITreeNodeObject {
id: TopLevelTreeNodeId;
name: string; // localizationString
}

View File

@@ -0,0 +1,32 @@
import { CipherType } from "jslib-common/enums/cipherType";
import { CipherStatus } from "./cipher-status.model";
export class VaultFilter {
cipherType?: CipherType;
selectedCollectionId?: string;
status?: CipherStatus;
selectedFolder = false; // This is needed because of how the "No Folder" folder works. It has a null id.
selectedFolderId?: string;
selectedOrganizationId?: string;
myVaultOnly = false;
refreshCollectionsAndFolders = false;
constructor(init?: Partial<VaultFilter>) {
Object.assign(this, init);
}
resetFilter() {
this.cipherType = null;
this.status = null;
this.selectedCollectionId = null;
this.selectedFolder = false;
this.selectedFolderId = null;
}
resetOrganization() {
this.myVaultOnly = false;
this.selectedOrganizationId = null;
this.resetFilter();
}
}

View File

@@ -0,0 +1,113 @@
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { Organization } from "jslib-common/models/domain/organization";
import { ITreeNodeObject } from "jslib-common/models/domain/treeNode";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from "jslib-common/models/view/folderView";
import { DynamicTreeNode } from "./models/dynamic-tree-node.model";
import { VaultFilter } from "./models/vault-filter.model";
import { VaultFilterService } from "./vault-filter.service";
@Directive()
export class VaultFilterComponent implements OnInit {
@Input() activeFilter: VaultFilter = new VaultFilter();
@Input() hideFolders = false;
@Input() hideCollections = false;
@Input() hideFavorites = false;
@Input() hideTrash = false;
@Input() hideOrganizations = false;
@Output() onFilterChange = new EventEmitter<VaultFilter>();
@Output() onAddFolder = new EventEmitter<never>();
@Output() onEditFolder = new EventEmitter<FolderView>();
isLoaded = false;
collapsedFilterNodes: Set<string>;
organizations: Organization[];
activePersonalOwnershipPolicy: boolean;
activeSingleOrganizationPolicy: boolean;
collections: DynamicTreeNode<CollectionView>;
folders: DynamicTreeNode<FolderView>;
constructor(protected vaultFilterService: VaultFilterService) {}
get displayCollections() {
return this.collections?.fullList != null && this.collections.fullList.length > 0;
}
async ngOnInit(): Promise<void> {
this.collapsedFilterNodes = await this.vaultFilterService.buildCollapsedFilterNodes();
this.organizations = await this.vaultFilterService.buildOrganizations();
if (this.organizations != null && this.organizations.length > 0) {
this.activePersonalOwnershipPolicy =
await this.vaultFilterService.checkForPersonalOwnershipPolicy();
this.activeSingleOrganizationPolicy =
await this.vaultFilterService.checkForSingleOrganizationPolicy();
}
this.folders = await this.vaultFilterService.buildFolders();
this.collections = await this.initCollections();
this.isLoaded = true;
}
// overwritten in web for organization vaults
async initCollections() {
return await this.vaultFilterService.buildCollections();
}
async toggleFilterNodeCollapseState(node: ITreeNodeObject) {
if (this.collapsedFilterNodes.has(node.id)) {
this.collapsedFilterNodes.delete(node.id);
} else {
this.collapsedFilterNodes.add(node.id);
}
await this.vaultFilterService.storeCollapsedFilterNodes(this.collapsedFilterNodes);
}
async applyFilter(filter: VaultFilter) {
if (filter.refreshCollectionsAndFolders) {
await this.reloadCollectionsAndFolders(filter);
filter = this.pruneInvalidatedFilterSelections(filter);
}
this.onFilterChange.emit(filter);
}
async reloadCollectionsAndFolders(filter: VaultFilter) {
this.folders = await this.vaultFilterService.buildFolders(filter.selectedOrganizationId);
this.collections = filter.myVaultOnly
? null
: await this.vaultFilterService.buildCollections(filter.selectedOrganizationId);
}
addFolder() {
this.onAddFolder.emit();
}
editFolder(folder: FolderView) {
this.onEditFolder.emit(folder);
}
protected pruneInvalidatedFilterSelections(filter: VaultFilter): VaultFilter {
filter = this.pruneInvalidFolderSelection(filter);
filter = this.pruneInvalidCollectionSelection(filter);
return filter;
}
protected pruneInvalidFolderSelection(filter: VaultFilter): VaultFilter {
if (filter.selectedFolder && !this.folders?.hasId(filter.selectedFolderId)) {
filter.selectedFolder = false;
filter.selectedFolderId = null;
}
return filter;
}
protected pruneInvalidCollectionSelection(filter: VaultFilter): VaultFilter {
if (
filter.selectedCollectionId != null &&
!this.collections?.hasId(filter.selectedCollectionId)
) {
filter.selectedCollectionId = null;
}
return filter;
}
}

View File

@@ -0,0 +1,82 @@
import { Injectable } from "@angular/core";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { PolicyType } from "jslib-common/enums/policyType";
import { Organization } from "jslib-common/models/domain/organization";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from "jslib-common/models/view/folderView";
import { DynamicTreeNode } from "./models/dynamic-tree-node.model";
@Injectable()
export class VaultFilterService {
constructor(
protected stateService: StateService,
protected organizationService: OrganizationService,
protected folderService: FolderService,
protected cipherService: CipherService,
protected collectionService: CollectionService,
protected policyService: PolicyService
) {}
async storeCollapsedFilterNodes(collapsedFilterNodes: Set<string>): Promise<void> {
await this.stateService.setCollapsedGroupings(Array.from(collapsedFilterNodes));
}
async buildCollapsedFilterNodes(): Promise<Set<string>> {
return new Set(await this.stateService.getCollapsedGroupings());
}
async buildOrganizations(): Promise<Organization[]> {
return await this.organizationService.getAll();
}
async buildFolders(organizationId?: string): Promise<DynamicTreeNode<FolderView>> {
const storedFolders = await this.folderService.getAllDecrypted();
let folders: FolderView[];
if (organizationId != null) {
const ciphers = await this.cipherService.getAllDecrypted();
const orgCiphers = ciphers.filter((c) => c.organizationId == organizationId);
folders = storedFolders.filter(
(f) =>
orgCiphers.filter((oc) => oc.folderId == f.id).length > 0 ||
ciphers.filter((c) => c.folderId == f.id).length < 1
);
} else {
folders = storedFolders;
}
const nestedFolders = await this.folderService.getAllNested(folders);
return new DynamicTreeNode<FolderView>({
fullList: folders,
nestedList: nestedFolders,
});
}
async buildCollections(organizationId?: string): Promise<DynamicTreeNode<CollectionView>> {
const storedCollections = await this.collectionService.getAllDecrypted();
let collections: CollectionView[];
if (organizationId != null) {
collections = storedCollections.filter((c) => c.organizationId === organizationId);
} else {
collections = storedCollections;
}
const nestedCollections = await this.collectionService.getAllNested(collections);
return new DynamicTreeNode<CollectionView>({
fullList: collections,
nestedList: nestedCollections,
});
}
async checkForSingleOrganizationPolicy(): Promise<boolean> {
return await this.policyService.policyAppliesToUser(PolicyType.SingleOrg);
}
async checkForPersonalOwnershipPolicy(): Promise<boolean> {
return await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership);
}
}

View File

@@ -0,0 +1,64 @@
import { Pipe, PipeTransform } from "@angular/core";
interface CardRuleEntry {
cardLength: number;
blocks: number[];
}
// See https://baymard.com/checkout-usability/credit-card-patterns for
// all possible credit card spacing patterns. For now, we just handle
// the below.
const numberFormats: Record<string, CardRuleEntry[]> = {
Visa: [{ cardLength: 16, blocks: [4, 4, 4, 4] }],
Mastercard: [{ cardLength: 16, blocks: [4, 4, 4, 4] }],
Maestro: [
{ cardLength: 16, blocks: [4, 4, 4, 4] },
{ cardLength: 13, blocks: [4, 4, 5] },
{ cardLength: 15, blocks: [4, 6, 5] },
{ cardLength: 19, blocks: [4, 4, 4, 4, 3] },
],
Discover: [{ cardLength: 16, blocks: [4, 4, 4, 4] }],
"Diners Club": [{ cardLength: 14, blocks: [4, 6, 4] }],
JCB: [{ cardLength: 16, blocks: [4, 4, 4, 4] }],
UnionPay: [
{ cardLength: 16, blocks: [4, 4, 4, 4] },
{ cardLength: 19, blocks: [6, 13] },
],
Amex: [{ cardLength: 15, blocks: [4, 6, 5] }],
Other: [{ cardLength: 16, blocks: [4, 4, 4, 4] }],
};
@Pipe({ name: "creditCardNumber" })
export class CreditCardNumberPipe implements PipeTransform {
transform(creditCardNumber: string, brand: string): string {
let rules = numberFormats[brand];
if (rules == null) {
rules = numberFormats["Other"];
}
const cardLength = creditCardNumber.length;
let matchingRule = rules.find((r) => r.cardLength == cardLength);
if (matchingRule == null) {
matchingRule = rules[0];
}
const blocks = matchingRule.blocks;
const chunks: string[] = [];
let total = 0;
blocks.forEach((c) => {
chunks.push(creditCardNumber.slice(total, total + c));
total += c;
});
// Append the remaining part
if (cardLength > total) {
chunks.push(creditCardNumber.slice(total));
}
return chunks.join(" ");
}
}

View File

@@ -159,11 +159,15 @@
<glyph unicode="&#xe982;" glyph-name="folder-closed-f" data-tags="bw-folder-closed-f" d="M589.227 683.038h340.031c25.254-0.32 49.35-10.594 67.084-28.583 17.733-17.987 27.687-42.248 27.659-67.503v-554.809c0.065-12.768-2.431-25.447-7.331-37.254-4.903-11.811-12.134-22.507-21.222-31.495-8.703-8.703-19.078-15.591-30.439-20.295s-23.558-7.107-35.878-7.107l-834.264 0.639c-25.285 0.191-49.485 10.369-67.279 28.39s-27.717 42.311-27.589 67.629v703.321c-0.128 25.285 9.762 49.547 27.525 67.567s41.927 28.233 67.212 28.457h364.868c25.285-0.192 49.485-10.37 67.279-28.39s27.717-42.317 27.589-67.63v-16.515c-0.191-9.411 3.361-18.532 9.858-25.382s15.46-10.818 24.901-11.042zM459.727 772.401l-364.868 0.037c-9.409-0.224-18.339-4.195-24.837-11.041s-10.019-15.972-9.795-25.382v-100.182c-0.051-0.737-0.079-1.479-0.079-2.23v-601.598c0-17.671 14.331-31.999 32-31.999s31.999 14.331 31.999 31.999v494.078c0 17.677 14.331 31.999 31.999 31.999h255.999c0.598 0 1.193-0.017 1.787-0.051h527.030c3.008-0.037 6.023 0.576 8.806 1.697s5.343 2.817 7.488 4.929c2.144 2.112 3.873 4.672 5.024 7.429s1.792 5.763 1.792 8.773v6.295c0.224 9.41-3.299 18.503-9.823 25.321s-15.465 10.755-24.873 10.979h-340.031c-25.285 0.289-49.485 10.498-67.244 28.486s-27.749 42.249-27.749 67.535v16.515c0.224 9.445-3.299 18.564-9.795 25.381s-15.426 10.789-24.837 11.042z" />
<glyph unicode="&#xe983;" glyph-name="providers" data-tags="bw-providers" d="M795.946 895.999c-68.137 0-123.373-55.236-123.373-123.373 0-26.887 8.6-51.764 23.2-72.032 5.421-7.527 5.928-17.864 0.072-25.057l-78.409-96.295c-5.865-7.204-16.129-8.871-24.347-4.54-33.264 17.525-71.159 27.444-111.37 27.444-65.26 0-124.418-26.127-167.584-68.491-6.851-6.723-17.598-7.916-25.311-2.203l-43.869 32.484c-7.393 5.474-9.491 15.502-6.223 24.099 5.178 13.62 8.013 28.396 8.013 43.833 0 68.137-55.236 123.373-123.373 123.373s-123.373-55.237-123.373-123.373c0-68.138 55.236-123.373 123.373-123.373 25.493 0 49.178 7.732 68.842 20.977 7.179 4.836 16.702 5.327 23.659 0.175l46.671-34.559c7.637-5.655 9.676-16.143 5.398-24.629-16.308-32.342-25.491-68.889-25.491-107.582 0-60.534 22.479-115.818 59.545-157.959 6.345-7.212 6.945-18.022 0.806-25.412l-52.717-63.466c-5.597-6.739-15.107-8.483-23.293-5.359-13.659 5.211-28.482 8.066-43.972 8.066-68.137 0-123.373-55.236-123.373-123.373s55.236-123.373 123.373-123.373c68.138 0 123.373 55.236 123.373 123.373 0 25.451-7.706 49.1-20.912 68.745-5.074 7.547-5.342 17.651 0.469 24.646l51.951 62.544c6.069 7.305 16.644 8.761 24.876 4.024 35.082-20.183 75.765-31.727 119.143-31.727 67.152 0 127.843 27.664 171.299 72.217 6.851 7.025 17.879 8.346 25.73 2.459l102.412-76.81c7.559-5.669 9.482-16.073 5.8-24.775-6.25-14.768-9.706-31.005-9.706-48.049 0-68.138 55.236-123.373 123.373-123.373s123.373 55.236 123.373 123.373c0 68.137-55.237 123.373-123.373 123.373-23.816 0-46.054-6.749-64.91-18.436-7.147-4.43-16.358-4.684-23.084 0.36l-109.29 81.968c-7.431 5.573-9.506 15.76-5.509 24.144 14.846 31.149 23.156 66.013 23.156 102.818 0 63.709-24.9 121.603-65.499 164.48-6.747 7.126-7.628 18.199-1.432 25.809l78.978 96.995c5.415 6.651 14.649 8.563 22.759 5.773 12.588-4.33 26.095-6.681 40.152-6.681 68.138 0 123.373 55.237 123.373 123.373s-55.236 123.373-123.373 123.373zM709.641 772.626c0 47.665 38.639 86.305 86.305 86.305s86.306-38.64 86.306-86.305c0-47.665-38.641-86.305-86.306-86.305s-86.305 38.639-86.305 86.305zM110.305 546.545c-41.677 6.139-73.666 42.052-73.666 85.435 0 47.696 38.665 86.361 86.361 86.361 13.198 0 25.705-2.961 36.891-8.254 29.41-13.755 49.785-43.609 49.785-78.22 0-47.665-38.641-86.306-86.306-86.306-4.441 0-8.804 0.336-13.065 0.983zM481.718 540.459c83.906 0 154.219-58.19 172.787-136.417 3.378-13.734 5.169-28.091 5.169-42.867 0-98.799-80.093-178.892-178.892-178.892s-178.892 80.093-178.892 178.892c0 78.88 51.053 145.837 121.919 169.629 18.151 6.258 37.633 9.655 57.908 9.655zM814.321 48.648c0 20.601 7.219 39.515 19.264 54.353 1.489 1.178 2.854 2.565 4.047 4.156 0.415 0.551 0.798 1.117 1.154 1.691 15.678 16.103 37.592 26.103 61.842 26.103 47.665 0 86.305-38.639 86.305-86.305s-38.639-86.306-86.305-86.306c-47.665 0-86.306 38.641-86.306 86.306zM96.513-4.628c0 47.665 38.64 86.305 86.305 86.305s86.306-38.639 86.306-86.305c0-47.665-38.641-86.305-86.306-86.305s-86.305 38.639-86.305 86.305z" />
<glyph unicode="&#xe984;" glyph-name="vault" data-tags="bw-vault" d="M418.067 645.284c-9.153 0-16.572-7.42-16.572-16.572v-57.561c0-5.573-4.163-10.223-9.627-11.318-29.41-5.9-55.303-21.534-74.176-43.397-3.666-4.249-9.823-5.568-14.686-2.762l-50.385 29.090c-7.925 4.576-18.061 1.861-22.637-6.066-4.576-7.925-1.861-18.061 6.065-22.637l50.881-29.377c4.78-2.76 6.741-8.612 5.036-13.863-4.189-12.901-6.453-26.672-6.453-40.97 0-15.287 2.587-29.97 7.349-43.635 1.849-5.305-0.085-11.304-4.95-14.112l-51.864-29.945c-7.925-4.576-10.641-14.712-6.065-22.637s14.712-10.642 22.637-6.065l52.418 30.263c4.776 2.759 10.821 1.533 14.514-2.565 18.697-20.758 43.859-35.576 72.314-41.287 5.463-1.096 9.627-5.746 9.627-11.318v-61.043c0-9.153 7.42-16.572 16.572-16.572s16.572 7.42 16.572 16.572v61.035c0 5.574 4.165 10.223 9.63 11.318 28.465 5.701 53.638 20.517 72.342 41.276 3.693 4.097 9.737 5.322 14.512 2.564l52.383-30.243c7.927-4.576 18.061-1.861 22.639 6.066 4.576 7.925 1.861 18.061-6.066 22.637l-51.82 29.918c-4.866 2.809-6.799 8.809-4.948 14.115 4.766 13.671 7.355 28.362 7.355 43.657 0 14.307-2.266 28.085-6.458 40.995-1.706 5.251 0.254 11.104 5.035 13.866l50.837 29.351c7.927 4.576 10.642 14.712 6.066 22.639-4.578 7.925-14.712 10.641-22.639 6.065l-50.351-29.070c-4.859-2.807-11.014-1.488-14.683 2.761-18.881 21.864-44.782 37.494-74.204 43.386-5.466 1.095-9.631 5.746-9.631 11.318v57.553c0 9.153-7.419 16.572-16.572 16.572zM517.524 429.848c0-54.914-44.517-99.432-99.432-99.432s-99.432 44.517-99.432 99.432 44.517 99.432 99.432 99.432c54.914 0 99.432-44.517 99.432-99.432zM119.798 683.951v-519.253c0-24.405 19.787-44.192 44.192-44.192h662.877c24.405 0 44.192 19.787 44.192 44.192v0.038c29.484 1.045 53.029 22.886 53.029 49.677v88.384c0 26.791-23.546 48.634-53.029 49.677v143.7c29.484 1.045 53.029 22.886 53.029 49.677v99.432c0 27.211-24.291 49.317-54.421 49.711-4.904 19.064-22.208 33.15-42.8 33.15h-662.877c-24.405 0-44.192-19.787-44.192-44.192zM163.99 694.999h662.877c6.102 0 11.048-4.946 11.048-11.048v-519.253c0-6.102-4.946-11.048-11.048-11.048h-662.877c-6.102 0-11.048 4.946-11.048 11.048v519.253c0 6.102 4.946 11.048 11.048 11.048zM871.058 316.177c0 1.643 1.435 2.936 3.022 2.515 7.622-2.029 13.182-8.377 13.182-15.893v-88.384c0-7.518-5.561-13.866-13.182-15.893-1.587-0.423-3.022 0.873-3.022 2.515v115.142zM874.080 529.958c-1.587-0.423-3.022 0.873-3.022 2.515v126.19c0 1.643 1.435 2.936 3.022 2.515 7.622-2.029 13.182-8.377 13.182-15.893v-99.432c0-7.518-5.561-13.866-13.182-15.893zM86.654 827.575h850.692c36.61 0 66.288-29.678 66.288-66.288v-673.924c0-31.251-21.624-57.45-50.726-64.45-5.591-1.346-10.038-6.036-10.038-11.785v-9.939c0-33.559-27.204-60.763-60.763-60.763h-66.288c-33.559 0-60.763 27.204-60.763 60.763v8.838c0 6.102-4.946 11.048-11.048 11.048h-464.014c-6.102 0-11.048-4.946-11.048-11.048v-8.838c0-33.559-27.204-60.763-60.763-60.763h-66.288c-33.559 0-60.763 27.204-60.763 60.763v9.939c0 5.749-4.448 10.439-10.039 11.785-29.102 7.001-50.726 33.2-50.726 64.45v673.924c0 36.61 29.678 66.288 66.288 66.288zM937.346 783.383h-850.692c-12.204 0-22.096-9.892-22.096-22.096v-673.924c0-12.204 9.892-22.096 22.096-22.096h850.692c12.204 0 22.096 9.892 22.096 22.096v673.924c0 12.204-9.892 22.096-22.096 22.096zM909.727 10.027c0 6.102-4.946 11.048-11.048 11.048h-99.432c-6.102 0-11.048-4.946-11.048-11.048v-8.838c0-15.255 12.366-27.619 27.619-27.619h66.288c15.255 0 27.619 12.366 27.619 27.619v8.838zM114.273 1.188c0-15.255 12.366-27.619 27.619-27.619h66.288c15.255 0 27.619 12.366 27.619 27.619v8.838c0 6.102-4.946 11.048-11.048 11.048h-99.432c-6.102 0-11.048-4.946-11.048-11.048v-8.838z" />
<glyph unicode="&#xe985;" glyph-name="browser" data-tags="bw-browser" d="M935.28 729.6h-846.557c-23.99-1.351-46.483-12.034-62.549-29.691s-24.403-40.874-23.226-64.565v-502.692c-1.177-23.722 7.158-46.91 23.226-64.566 16.065-17.658 38.529-28.339 62.549-29.691h846.304c24.053 1.289 46.578 11.939 62.711 29.598s24.5 40.905 23.323 64.658v502.692c1.177 23.722-7.158 46.909-23.226 64.565s-38.562 28.339-62.549 29.691zM935.28 666.763c7.987-0.47 15.43-4.054 20.774-9.93s8.114-13.603 7.731-21.489v-18.375c0.383-6.819-2.003-13.478-6.616-18.538-4.614-5.059-11.041-8.138-17.944-8.547h-854.512c-6.874 0.408-13.3 3.488-17.912 8.547-4.614 5.060-6.968 11.72-6.585 18.538v18.375c-0.383 7.887 2.386 15.614 7.731 21.489s12.791 9.455 20.774 9.93h846.557zM935.28 101.613h-846.557c-7.922 0.471-15.303 3.991-20.648 9.802s-8.146 13.415-7.857 21.238v366.938c-0.351 6.819 2.003 13.477 6.616 18.537s11.041 8.138 17.944 8.547h854.576c6.905-0.376 13.333-3.457 17.944-8.547s7-11.75 6.616-18.537v-366.938c0.383-7.919-2.417-15.646-7.762-21.52-5.345-5.876-12.854-9.425-20.871-9.898v0.376zM662.415 234.057l-47.904 130.649c-3.818 10.406 6.572 20.378 16.813 16.141l125.055-51.748c11.021-4.563 10.276-20.417-1.122-23.923l-42.823-13.175c-3.838-1.183-6.892-4.103-8.242-7.884l-17.845-49.959c-4.002-11.213-19.834-11.279-23.933-0.102z" />
<glyph unicode="&#xe985;" glyph-name="browser" data-tags="bw-browser" d="M935.28 697.6h-846.557c-23.99-1.351-46.483-12.034-62.549-29.691s-24.403-40.874-23.226-64.565v-502.692c-1.177-23.722 7.158-46.91 23.226-64.566 16.065-17.658 38.529-28.339 62.549-29.691h846.304c24.053 1.289 46.578 11.939 62.711 29.598s24.5 40.905 23.323 64.658v502.692c1.177 23.722-7.158 46.909-23.226 64.565s-38.562 28.339-62.549 29.691zM935.28 634.763c7.987-0.47 15.43-4.054 20.774-9.93s8.114-13.603 7.731-21.489v-18.375c0.383-6.819-2.003-13.478-6.616-18.538-4.614-5.059-11.041-8.138-17.944-8.547h-854.512c-6.874 0.408-13.3 3.488-17.912 8.547-4.614 5.060-6.968 11.72-6.585 18.538v18.375c-0.383 7.887 2.386 15.614 7.731 21.489s12.791 9.455 20.774 9.93h846.557zM935.28 69.613h-846.557c-7.922 0.471-15.303 3.991-20.648 9.802s-8.146 13.415-7.857 21.238v366.938c-0.351 6.819 2.003 13.477 6.616 18.537s11.041 8.138 17.944 8.547h854.576c6.905-0.376 13.333-3.457 17.944-8.547s7-11.75 6.616-18.537v-366.938c0.383-7.919-2.417-15.646-7.762-21.52-5.345-5.876-12.854-9.425-20.871-9.898v0.376zM662.415 234.057l-47.904 130.649c-3.818 10.406 6.572 20.378 16.813 16.141l125.055-51.748c11.021-4.563 10.276-20.417-1.122-23.923l-42.823-13.175c-3.838-1.183-6.892-4.103-8.242-7.884l-17.845-49.959c-4.002-11.213-19.834-11.279-23.933-0.102z" />
<glyph unicode="&#xe986;" glyph-name="mobile" data-tags="bw-mobile" d="M517.369-31.195c14.501 0 26.256 11.756 26.256 26.256s-11.755 26.256-26.256 26.256-26.256-11.755-26.256-26.256c0-14.5 11.755-26.256 26.256-26.256zM239.492 861.091c16.574 18.214 39.779 29.234 64.526 30.627h415.965c24.747-1.393 47.951-12.413 64.525-30.627s25.173-42.166 23.958-66.603v-175.116c0.151-3.031 0.152-6.081 0-9.14v-452.461c0.152-3.059 0.151-6.11 0-9.141v-175.116c1.216-24.438-7.385-48.389-23.958-66.603s-39.778-29.234-64.525-30.628h-415.965c-24.747 1.394-47.952 12.414-64.526 30.628s-25.173 42.165-23.958 66.603v175.13c-0.151 3.027-0.152 6.072 0 9.127v452.461c-0.152 3.059-0.151 6.109 0 9.14v175.116c-1.215 24.437 7.385 48.389 23.958 66.603zM284.75 816.656c-5.514-6.060-8.369-14.034-7.976-22.168v-32.082c-0.393-7.033 2.069-13.904 6.827-19.123 4.76-5.217 11.389-8.394 18.511-8.816h419.776c7.122 0.421 13.752 3.598 18.511 8.816s7.221 12.090 6.828 19.123v32.082c0.393 8.135-2.462 16.108-7.977 22.168-5.514 6.061-13.194 9.756-21.433 10.242h-411.634c-8.239-0.486-15.918-4.181-21.433-10.242zM301.242 112.908c0.917-0.137 1.843-0.234 2.776-0.289h415.965c0.932 0.055 1.856 0.154 2.774 0.29 6.372 1.386 12.193 4.696 16.591 9.53 4.853 5.335 7.672 12.153 8.010 19.28 0.045 0.958 0.045 1.922 0 2.888l-0.003 0.034v494.801c-0.343 7.114-3.161 13.919-8.007 19.246-4.398 4.835-10.216 8.145-16.59 9.532-0.916 0.135-1.843 0.232-2.775 0.288h-415.965c-0.933-0.055-1.857-0.154-2.774-0.29-6.373-1.386-12.194-4.695-16.591-9.53-4.846-5.327-7.665-12.132-8.008-19.246v-497.756c0.343-7.115 3.162-13.92 8.008-19.247 4.398-4.834 10.216-8.145 16.59-9.531zM739.25-48.655c5.515 6.060 8.369 14.034 7.977 22.168v45.21c0.393 7.034-2.069 13.905-6.828 19.123s-11.389 8.394-18.511 8.816h-419.776c-7.122-0.421-13.752-3.597-18.511-8.816-4.758-5.217-7.22-12.088-6.827-19.123v-45.21c-0.393-8.134 2.462-16.108 7.976-22.168s13.194-9.756 21.433-10.243h411.637c8.237 0.487 15.917 4.181 21.43 10.243z" />
<glyph unicode="&#xe987;" glyph-name="cli" data-tags="bw-cli" d="M564.293 401.426c8.527 6.203 20.465 4.315 26.668-4.209l76.359-104.993c4.994-6.865 4.854-16.203-0.343-22.912l-76.359-98.631c-6.453-8.337-18.443-9.864-26.78-3.408-8.335 6.453-9.862 18.443-3.407 26.78l67.589 87.302-67.932 93.405c-6.203 8.527-4.315 20.465 4.209 26.666zM709.15 207.82c-10.543 0-19.089-8.548-19.089-19.089s8.548-19.089 19.089-19.089h139.99c10.543 0 19.089 8.548 19.089 19.089s-8.548 19.089-19.089 19.089h-139.99zM935.28 729.6h-846.557c-23.99-1.351-46.483-12.034-62.549-29.691s-24.403-40.874-23.226-64.565v-502.692c-1.177-23.722 7.158-46.91 23.226-64.566 16.065-17.658 38.529-28.339 62.549-29.691h846.304c24.053 1.289 46.578 11.939 62.711 29.598s24.5 40.905 23.323 64.658v502.692c1.177 23.722-7.158 46.909-23.226 64.565s-38.562 28.339-62.549 29.691zM935.28 666.763c7.987-0.47 15.43-4.054 20.774-9.93s8.114-13.603 7.731-21.489v-18.375c0.383-6.819-2.003-13.478-6.616-18.538-4.614-5.059-11.041-8.138-17.944-8.547h-854.512c-6.874 0.408-13.3 3.488-17.912 8.547-4.614 5.060-6.968 11.72-6.585 18.538v18.375c-0.383 7.887 2.386 15.614 7.731 21.489s12.791 9.455 20.774 9.93h846.557zM935.28 101.613h-846.557c-7.922 0.471-15.303 3.991-20.648 9.802s-8.146 13.415-7.857 21.238v366.938c-0.351 6.819 2.003 13.477 6.616 18.537s11.041 8.138 17.944 8.547h854.576c6.905-0.376 13.333-3.457 17.944-8.547s7-11.75 6.616-18.537v-366.938c0.383-7.919-2.417-15.646-7.762-21.52-5.345-5.876-12.854-9.425-20.871-9.898v0.376z" />
<glyph unicode="&#xe988;" glyph-name="save-changes" data-tags="bw-save-changes" d="M37.926 839.11v-910.222c0-31.42 25.47-56.889 56.889-56.889h834.37c31.42 0 56.889 25.469 56.889 56.889v754.218c0 26.878-11.408 52.494-31.387 70.475l-131.199 118.079c-17.41 15.669-40.004 24.34-63.427 24.34h-665.245c-31.419 0-56.889-25.47-56.889-56.889zM884.983 688.755c3.995-3.595 6.277-8.719 6.277-14.095v-688.883c0-10.473-8.49-18.963-18.963-18.963h-37.926c-10.473 0-18.963 8.49-18.963 18.963v334.127c0 14.454-12.736 26.169-28.444 26.169h-549.926c-15.709 0-28.444-11.715-28.444-26.169v-334.127c0-10.473-8.49-18.963-18.963-18.963h-37.926c-10.473 0-18.963 8.49-18.963 18.963v796.444c0 10.473 8.49 18.963 18.963 18.963h37.926c10.473 0 18.963-8.49 18.963-18.963v-199.111c0-15.709 12.245-28.444 27.35-28.444h476.262c15.106 0 27.35 12.736 27.35 28.444v203.255c0 8.184 6.635 14.819 14.82 14.819 3.66 0 7.191-1.355 9.912-3.804l120.695-108.626zM282.256 611.555c-10.473 0-18.963 8.49-18.963 18.963v170.667h229.744c10.473 0 18.963-8.49 18.963-18.963v-75.852c0-20.946 16.979-37.926 37.926-37.926h37.926c20.946 0 37.926 16.979 37.926 37.926v75.852c0 10.473 8.49 18.963 18.963 18.963h21.151c10.473 0 18.963-8.49 18.963-18.963v-151.704c0-10.473-8.49-18.963-18.963-18.963h-383.636zM265.482-14.223v288.996c0 10.473 8.49 18.963 18.963 18.963h474.074v-307.959c0-10.473-8.49-18.963-18.963-18.963h-455.111c-10.473 0-18.963 8.49-18.963 18.963z" />
<glyph unicode="&#xe989;" glyph-name="numbered-list" data-tags="bw-numbered-list" d="M128.654 569.533v138.823c-29.309-19.833-49.039-29.749-59.194-29.749-4.847 0-9.173 1.679-12.981 5.037-3.693 3.458-5.539 7.424-5.539 11.898 0 5.186 1.846 8.999 5.539 11.438s10.213 5.593 19.557 9.459c13.963 5.798 25.097 11.898 33.405 18.306 8.424 6.406 15.867 13.577 22.328 21.507 6.463 7.935 10.675 12.814 12.636 14.646s5.654 2.748 11.079 2.748c6.115 0 11.019-2.085 14.711-6.256 3.693-4.169 5.539-9.916 5.539-17.239v-174.673c0-20.441-7.903-30.662-23.711-30.662-7.039 0-12.692 2.084-16.962 6.256-4.271 4.169-6.405 10.322-6.405 18.459zM107.712 309.403h90.35c9.001 0 15.867-1.629 20.599-4.883 4.729-3.253 7.097-7.676 7.097-13.273 0-4.983-1.903-9.204-5.711-12.662-3.692-3.458-9.348-5.186-16.962-5.186h-127.391c-8.655 0-15.405 2.084-20.251 6.255-4.846 4.273-7.27 9.255-7.27 14.95 0 3.663 1.557 8.491 4.673 14.492 3.116 6.103 6.519 10.883 10.214 14.342 15.346 14.035 29.195 26.035 41.542 36.002 12.347 10.069 21.175 16.68 26.482 19.833 9.462 5.897 17.307 11.797 23.539 17.695 6.346 6.002 11.134 12.103 14.367 18.306 3.347 6.305 5.019 12.458 5.019 18.459 0 6.511-1.789 12.307-5.366 17.391-3.461 5.187-8.252 9.204-14.367 12.053-6.002 2.847-12.579 4.273-19.731 4.273-15.116 0-27.003-5.847-35.655-17.545-1.155-1.525-3.116-5.696-5.885-12.509-2.654-6.815-5.711-12.053-9.173-15.712-3.348-3.661-8.308-5.491-14.887-5.491-5.77 0-10.557 1.679-14.366 5.033-3.81 3.357-5.713 7.935-5.713 13.73 0 7.017 1.789 14.341 5.366 21.967s8.886 14.542 15.925 20.747c7.156 6.205 16.156 11.188 27.003 14.952 10.962 3.864 23.77 5.796 38.426 5.796 17.656 0 32.714-2.439 45.176-7.322 8.076-3.253 15.174-7.729 21.289-13.423s10.847-12.307 14.194-19.832c3.461-7.425 5.192-15.152 5.192-23.187 0-12.611-3.578-24.105-10.731-34.476-7.038-10.272-14.25-18.358-21.636-24.255-7.384-5.798-19.789-14.952-37.213-27.459-17.307-12.509-29.195-22.223-35.655-29.138-2.769-2.747-5.598-6.051-8.482-9.914zM125.712 133.175c10.615 0 19.731 2.748 27.348 8.239 7.731 5.492 11.596 13.373 11.596 23.646 0 7.831-3.058 14.542-9.173 20.137-6.115 5.697-14.367 8.544-24.753 8.544-7.039 0-12.868-0.865-17.483-2.593-4.498-1.728-8.076-4.017-10.73-6.866-2.654-2.847-5.191-6.51-7.614-10.983-2.309-4.477-4.444-8.696-6.404-12.662-1.156-2.135-3.231-3.814-6.232-5.037-3-1.22-6.464-1.83-10.385-1.83-4.615 0-8.886 1.628-12.809 4.882-3.809 3.358-5.711 7.781-5.711 13.273 0 5.289 1.788 10.832 5.366 16.63 3.692 5.897 8.999 11.493 15.924 16.78 7.039 5.287 15.753 9.51 26.138 12.662 10.385 3.253 21.982 4.882 34.789 4.882 11.192 0 21.406-1.373 30.637-4.118 9.231-2.644 17.251-6.51 24.059-11.594 6.809-5.085 11.943-10.984 15.405-17.695s5.192-13.933 5.192-21.663c0-10.17-2.537-18.914-7.614-26.239-4.96-7.22-12.117-14.29-21.465-21.203 9.001-4.273 16.558-9.155 22.675-14.647 6.231-5.491 10.902-11.594 14.018-18.305 3.116-6.611 4.673-13.781 4.673-21.512 0-9.254-2.133-18.204-6.404-26.849-4.155-8.644-10.329-16.374-18.521-23.186-8.193-6.712-17.945-12.002-29.251-15.867-11.192-3.763-23.598-5.645-37.213-5.645-13.846 0-26.252 2.187-37.212 6.559s-20.020 9.815-27.176 16.325c-7.038 6.61-12.406 13.423-16.097 20.441-3.578 7.017-5.366 12.814-5.366 17.392 0 5.897 2.133 10.628 6.404 14.187 4.386 3.661 9.81 5.491 16.269 5.491 3.231 0 6.347-0.865 9.348-2.593 2.999-1.628 4.96-3.609 5.885-5.951 6-14.136 12.405-24.662 19.212-31.577 6.925-6.816 16.617-10.221 29.078-10.221 7.156 0 14.019 1.525 20.599 4.578 6.693 3.153 12.174 7.779 16.441 13.882 4.386 6.102 6.577 13.171 6.577 21.203 0 11.898-3.692 21.204-11.078 27.917-7.384 6.816-17.655 10.221-30.809 10.221-2.309 0-5.885-0.204-10.73-0.609s-7.963-0.609-9.348-0.609c-6.347 0-11.251 1.373-14.712 4.118-3.461 2.848-5.191 6.763-5.191 11.748 0 4.883 2.075 8.796 6.231 11.748 4.155 3.052 10.329 4.577 18.521 4.577h7.098zM356.64 680.597h607.317c15.6 0 28.248-12.647 28.248-28.248s-12.647-28.248-28.248-28.248h-607.317c-15.6 0-28.248 12.647-28.248 28.248s12.647 28.248 28.248 28.248zM356.64 412.248h607.317c15.6 0 28.248-12.647 28.248-28.248s-12.647-28.248-28.248-28.248h-607.317c-15.6 0-28.248 12.647-28.248 28.248s12.647 28.248 28.248 28.248zM356.64 129.776h607.317c15.6 0 28.248-12.647 28.248-28.248s-12.647-28.248-28.248-28.248h-607.317c-15.6 0-28.248 12.647-28.248 28.248s12.647 28.248 28.248 28.248z" />
<glyph unicode="&#xe98a;" glyph-name="bwi-billing" data-tags="bwi-billing" d="M64 800v-832c0-53.018 42.98-96 96-96h704c53.018 0 96 42.982 96 96v551.912c0 25.756-10.349 50.43-28.723 68.48l-285.088 280.088c-17.953 17.637-42.113 27.52-67.279 27.52h-418.91c-53.020 0-96-42.981-96-96zM886.426 542.739c6.125-6.017 9.574-14.242 9.574-22.827v-551.912c0-17.67-14.33-32-32-32h-704c-17.673 0-32 14.33-32 32v832c0 17.673 14.327 32 32 32h418.91c8.388 0 16.442-3.294 22.426-9.173l285.089-280.088zM608 864c-17.673 0-32-14.327-32-32v-256c0-35.346 28.654-64 64-64h256c17.67 0 32 14.327 32 32s-14.33 32-32 32h-256v256c0 17.673-14.327 32-32 32zM352 583.111c-53.020 0-96-38.205-96-85.333s42.98-85.334 96-85.334c88.365 0 160-63.675 160-142.222s-71.635-142.222-160-142.222c-77.479 0-142.095 48.954-156.841 113.958-3.493 15.4 11.168 28.264 28.841 28.264s31.478-13.084 37.26-27.931c13.014-33.408 48.724-57.402 90.74-57.402 53.020 0 96 38.202 96 85.332 0 47.128-42.98 85.334-96 85.334-88.365 0-160 63.675-160 142.222s71.635 142.222 160 142.222c77.479 0 142.095-48.952 156.841-113.96 3.493-15.399-11.168-28.262-28.841-28.262s-31.478 13.084-37.26 27.93c-13.014 33.412-48.724 57.404-90.74 57.404zM608 448c-17.673 0-32-14.327-32-32s14.327-32 32-32h128c17.67 0 32 14.327 32 32s-14.33 32-32 32h-128zM576 288c0 17.673 14.327 32 32 32h128c17.67 0 32-14.327 32-32s-14.33-32-32-32h-128c-17.673 0-32 14.327-32 32zM608 192c-17.673 0-32-14.33-32-32s14.327-32 32-32h128c17.67 0 32 14.33 32 32s-14.33 32-32 32h-128zM352 768c-17.673 0-32-14.327-32-32 0-37.333 0-74.667 0-112 0-17.673 14.327-32 32-32s32 14.327 32 32c0 37.333 0 74.667 0 112 0 17.673-14.327 32-32 32zM352 176c-17.673 0-32-14.33-32-32 0-37.331 0-74.669 0-112 0-17.67 14.327-32 32-32s32 14.33 32 32c0 37.331 0 74.669 0 112 0 17.67-14.327 32-32 32z" />
<glyph unicode="&#xe98b;" glyph-name="bwi-family" data-tags="bwi-family" d="M876.16 417.364c33.907 23.117 59.514 56.49 73.069 95.225 13.549 38.734 14.336 80.792 2.24 120.006s-36.435 73.52-69.453 97.889c-33.024 24.369-72.979 37.517-114.016 37.517s-80.992-13.148-114.016-37.517c-33.015-24.369-57.357-58.675-69.453-97.889s-11.31-81.272 2.243-120.006c13.553-38.735 39.16-72.108 73.066-95.225-14.515-7.995-28.049-17.66-40.32-28.8-11.604 17.965-27.524 32.735-46.306 42.963s-39.828 15.587-61.214 15.587c-21.386 0-42.432-5.359-61.214-15.587s-34.702-24.998-46.306-42.963c-12.433 10.938-25.942 20.587-40.32 28.8 33.906 23.117 59.513 56.49 73.066 95.225s14.339 80.792 2.243 120.006c-12.096 39.213-36.438 73.52-69.457 97.889s-72.976 37.517-114.013 37.517c-41.037 0-80.995-13.148-114.013-37.517s-57.361-58.675-69.457-97.889c-12.096-39.214-11.31-81.272 2.243-120.006s39.16-72.108 73.066-95.225c-41.25-25.997-76.013-61.078-101.636-102.561s-41.426-88.275-46.204-136.799c0-21.76 5.76-49.92 27.52-49.92h316.8c-12.806-27.712-21.020-57.325-24.32-87.68 0-17.28 4.48-40.32 20.48-40.32h334.080c20.48 0 31.36 14.72 28.16 40.32-3.482 30.17-11.238 59.693-23.040 87.68h305.92c27.52 0 41.6 17.92 37.76 49.92-4.704 48.467-20.41 95.223-45.92 136.703s-60.147 76.59-101.28 102.658zM640 576.084c0 25.316 7.507 50.063 21.574 71.113 14.061 21.050 34.054 37.455 57.44 47.144 23.392 9.688 49.126 12.223 73.958 7.284 24.826-4.939 47.635-17.13 65.536-35.031s30.093-40.708 35.034-65.538c4.934-24.829 2.4-50.566-7.283-73.955-9.69-23.389-26.099-43.38-47.149-57.444s-45.792-21.572-71.11-21.572c-33.946 0-66.502 13.485-90.509 37.491s-37.491 56.562-37.491 90.509zM512 384.084c14.449 0.495 28.639-3.917 40.261-12.516s19.988-20.881 23.739-34.844c0.664-5.527 0.664-11.113 0-16.64-0.136-13.043-4.255-25.734-11.804-36.371s-18.168-18.714-30.436-23.149c-6.873-2.958-14.277-4.484-21.76-4.484s-14.887 1.526-21.76 4.484c-12.268 4.435-22.886 12.511-30.436 23.149s-11.668 23.328-11.804 36.371c-0.608 5.53-0.608 11.11 0 16.64 3.75 13.962 12.118 26.244 23.739 34.844s25.812 13.011 40.261 12.516zM128 576.084c0 25.316 7.507 50.063 21.572 71.113s34.056 37.455 57.444 47.144c23.389 9.688 49.126 12.223 73.955 7.284s47.637-17.13 65.538-35.031c17.901-17.901 30.092-40.708 35.031-65.538s2.404-50.566-7.284-73.955c-9.688-23.389-26.094-43.38-47.144-57.444s-45.797-21.572-71.113-21.572c-33.948 0-66.505 13.485-90.509 37.491s-37.491 56.562-37.491 90.509zM67.2 192.083c23.68 119.041 106.88 192.001 192 192.001 24.129-1.312 47.711-7.688 69.213-18.715s40.442-26.456 55.587-45.285c0.234-17.18 3.925-34.137 10.852-49.86 6.927-15.721 16.949-29.891 29.468-41.66-15.103-10.182-28.678-22.47-40.32-36.48h-316.8zM634.24 64.083h-244.48c5.257 22.605 14.117 44.211 26.24 64 9.496 17.747 23.227 32.877 39.974 44.038 16.747 11.168 35.992 18.022 56.026 19.962h6.4c19.405-2.374 37.939-9.446 53.997-20.602 16.058-11.149 29.157-26.048 38.163-43.398 11.484-19.827 19.493-41.472 23.68-64zM640 192.083c-11.731 13.562-25.302 25.408-40.32 35.2 12.674 11.917 22.786 26.291 29.718 42.248 6.932 15.955 10.54 33.156 10.602 50.553 15.584 19.129 35.053 34.728 57.12 45.761s46.227 17.251 70.88 18.239c48.602-5.637 93.792-27.776 128.038-62.719s55.469-80.577 60.122-129.281h-316.16z" />
<glyph unicode="&#xe98c;" glyph-name="bwi-provider" data-tags="bwi-provider" d="M384 704v-64h256v64h-256zM320 736c0 17.673 14.327 32 32 32h320c17.67 0 32-14.327 32-32v-96h288c17.67 0 32-14.327 32-32v-640c0-17.67-14.33-32-32-32h-960c-17.673 0-32 14.33-32 32v640c0 17.673 14.327 32 32 32h288v96zM960 512v64h-896v-64c0-70.692 57.308-128 128-128h192v32c0 17.673 14.327 32 32 32h192c17.673 0 32-14.327 32-32v-32h192c70.694 0 128 57.308 128 128zM640 320v-32c0-17.673-14.327-32-32-32h-192c-17.673 0-32 14.327-32 32v32h-192c-49.174 0-94.031 18.486-128 48.889v-368.889h896v368.889c-33.971-30.403-78.822-48.889-128-48.889h-192zM448 384v-64h128v64h-128z" />
<glyph unicode="&#xe98d;" glyph-name="bwi-business" data-tags="bwi-business" d="M384 736c0 17.673 14.327 32 32 32h64c17.673 0 32-14.327 32-32s-14.327-32-32-32h-64c-17.673 0-32 14.327-32 32zM576 736c0 17.673 14.327 32 32 32h64c17.67 0 32-14.327 32-32s-14.33-32-32-32h-64c-17.673 0-32 14.327-32 32zM768 736c0 17.673 14.33 32 32 32h64c17.67 0 32-14.327 32-32s-14.33-32-32-32h-64c-17.67 0-32 14.327-32 32zM576 544c0 17.673 14.327 32 32 32h64c17.67 0 32-14.327 32-32s-14.33-32-32-32h-64c-17.673 0-32 14.327-32 32zM384 544c0 17.673 14.327 32 32 32h64c17.673 0 32-14.327 32-32s-14.327-32-32-32h-64c-17.673 0-32 14.327-32 32zM768 544c0 17.673 14.33 32 32 32h64c17.67 0 32-14.327 32-32s-14.33-32-32-32h-64c-17.67 0-32 14.327-32 32zM576 352c0 17.673 14.327 32 32 32h64c17.67 0 32-14.327 32-32s-14.33-32-32-32h-64c-17.673 0-32 14.327-32 32zM768 352c0 17.673 14.33 32 32 32h64c17.67 0 32-14.327 32-32s-14.33-32-32-32h-64c-17.67 0-32 14.327-32 32zM576 160c0 17.67 14.327 32 32 32h64c17.67 0 32-14.33 32-32s-14.33-32-32-32h-64c-17.673 0-32 14.33-32 32zM768 160c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32s-14.33-32-32-32h-64c-17.67 0-32 14.33-32 32zM928 896c53.018 0 96-42.981 96-96v-704c0-53.018-42.982-96-96-96h-435.039c8.646-24.486 14.886-50.579 18.353-77.805 4.093-32.154-10.337-50.195-37.797-50.195h-445.465c-21.458 0-30.33 28.429-27.559 50.195 13.575 106.63 69.703 195.77 146.192 239.942-49.966 34.662-82.685 92.442-82.685 157.862 0 106.038 85.962 192 192 192v288c0 53.019 42.98 96 96 96h576zM128 320c0-70.694 57.308-128 128-128s128 57.306 128 128c0 70.692-57.308 128-128 128s-128-57.308-128-128zM320 800v-298.925c54.554-19.282 97.793-62.52 117.075-117.075h42.925c17.673 0 32-14.327 32-32s-14.327-32-32-32h-32c0-65.459-32.757-123.264-82.772-157.926 38.691-22.368 72.167-56.25 97.553-98.074h465.219c17.67 0 32 14.33 32 32v704c0 17.673-14.33 32-32 32h-576c-17.673 0-32-14.327-32-32zM255.903 128c-81.62 0-165.099-72.646-188.403-192h376.807c-23.304 119.354-106.783 192-188.404 192z" />
<glyph unicode="&#xe9ee;" glyph-name="rocket" data-tags="rocket" d="M650.515 648.267c33.538 33.532 87.904 33.532 121.443 0 33.538-33.538 33.538-87.904 0-121.443s-87.904-33.538-121.443 0c-33.532 33.538-33.532 87.904 0 121.443zM750.801 627.113c-21.855 21.856-57.284 21.856-79.134 0-21.856-21.855-21.856-57.284 0-79.134 21.855-21.855 57.284-21.855 79.134 0s21.855 57.284 0 79.134zM493.141 680.645c113.184 90.608 223.416 148.836 310.181 180.552 43.273 15.818 81.527 25.336 111.933 28.691 15.138 1.668 29.339 1.929 41.709 0.19 11.507-1.615 25.903-5.552 36.583-16.232l3.981-3.981c10.68-10.679 14.617-25.076 16.232-36.582 1.739-12.377 1.478-26.572-0.19-41.71-3.356-30.406-12.874-68.659-28.691-111.932-31.716-86.771-89.944-196.998-180.552-310.187-29.487-36.837-59.902-73.855-91.847-111.076l30.789-145.412c4.388-20.715-0.21-42.314-12.654-59.447l-89.489-123.202c-35.299-48.598-110.586-37.857-130.894 18.67l-31.576 87.886-74.269-74.269c-5.844-5.844-15.313-5.844-21.151 0-5.844 5.845-5.844 15.313 0 21.152l80.747 80.747-47.795 47.794-162.229-162.229c-5.844-5.844-15.313-5.844-21.151 0s-5.844 15.313 0 21.151l162.229 162.229-54.059 54.060-306.392-306.392c-5.844-5.844-15.313-5.844-21.151 0-5.844 5.845-5.844 15.313 0 21.151l306.391 306.392-54.059 54.060-61.939-61.939c-5.845-5.844-15.313-5.844-21.151 0-5.845 5.844-5.845 15.313 0 21.151l61.938 61.939-47.794 47.794-112.086-112.086c-5.844-5.844-15.312-5.844-21.151 0-5.844 5.845-5.844 15.313 0 21.152l109.675 109.669-100.568 36.13c-56.526 20.307-67.261 95.6-18.663 130.894l123.202 89.489c17.132 12.444 38.73 17.041 59.446 12.654l145.412-30.789c37.221 31.945 74.238 62.36 111.076 91.847zM952.797 829.985c-0.744 0.223-2.062 0.551-4.14 0.84-5.79 0.812-14.652 0.934-26.837-0.411-24.234-2.675-57.643-10.683-97.952-25.414-80.389-29.379-184.973-84.318-293.329-171.062-38.013-30.432-76.092-61.749-114.295-94.665-2.919-4.249-6.879-7.644-11.433-9.895-54.88-47.714-110.027-98.85-165.607-155.478l258.391-258.39c56.628 55.58 107.765 110.728 155.48 165.609 2.25 4.551 5.643 8.51 9.89 11.427 32.912 38.205 64.235 76.284 94.667 114.298 86.744 108.362 141.683 212.941 171.063 293.329 14.73 40.309 22.744 73.723 25.414 97.952 1.345 12.179 1.222 21.047 0.411 26.837-0.29 2.078-0.619 3.397-0.84 4.14l-0.875 0.875zM224.258 561.056c-4.141 0.875-8.464-0.045-11.889-2.533l-123.202-89.489c-9.719-7.058-7.573-22.117 3.736-26.177l96.234-34.573c45.935 47.161 91.519 90.548 136.803 131.242l-101.677 21.529zM663.214 202.25c-40.694-45.278-84.081-90.868-131.243-136.803l34.574-96.235c4.061-11.304 19.119-13.455 26.177-3.736l89.489 123.202c2.488 3.425 3.408 7.748 2.533 11.889l-21.529 101.678z" />
<glyph unicode="&#xe9ef;" glyph-name="ellipsis-h" data-tags="ellipsis-h" d="M919.751 314.596c-43.122 0-78.080 34.958-78.080 78.080s34.958 78.080 78.080 78.080c43.122 0 78.080-34.958 78.080-78.080s-34.958-78.080-78.080-78.080zM520.676 314.596c-43.122 0-78.080 34.958-78.080 78.080s34.958 78.080 78.080 78.080c43.122 0 78.080-34.958 78.080-78.080s-34.958-78.080-78.080-78.080zM121.6 314.596c-43.122 0-78.080 34.958-78.080 78.080s34.958 78.080 78.080 78.080c43.122 0 78.080-34.958 78.080-78.080s-34.958-78.080-78.080-78.080z" />
<glyph unicode="&#xe9f0;" glyph-name="ellipsis-v" data-tags="ellipsis-v" d="M586.472 734.020c0-37.017-30.007-67.024-67.024-67.024s-67.024 30.007-67.024 67.024c0 37.017 30.007 67.024 67.024 67.024s67.024-30.007 67.024-67.024zM586.472 391.448c0-37.017-30.007-67.024-67.024-67.024s-67.024 30.007-67.024 67.024c0 37.017 30.007 67.024 67.024 67.024s67.024-30.007 67.024-67.024zM586.472 48.872c0-37.017-30.007-67.024-67.024-67.024s-67.024 30.007-67.024 67.024c0 37.017 30.007 67.024 67.024 67.024s67.024-30.007 67.024-67.024z" />

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 269 KiB

View File

@@ -241,6 +241,10 @@ $icons: (
"android": "\e944",
"error": "\e981",
"numbered-list": "\e989",
"billing": "\e98a",
"family": "\e98b",
"provider": "\e98c",
"business": "\e98d",
);
@each $name, $glyph in $icons {

View File

@@ -1,4 +1,4 @@
import { Injector, LOCALE_ID, NgModule } from "@angular/core";
import { InjectionToken, Injector, LOCALE_ID, NgModule } from "@angular/core";
import { ApiService as ApiServiceAbstraction } from "jslib-common/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "jslib-common/abstractions/appId.service";
@@ -74,28 +74,69 @@ import { UsernameGenerationService } from "jslib-common/services/usernameGenerat
import { VaultTimeoutService } from "jslib-common/services/vaultTimeout.service";
import { WebCryptoFunctionService } from "jslib-common/services/webCryptoFunction.service";
import { AuthGuardService } from "./auth-guard.service";
import { AuthGuard } from "../guards/auth.guard";
import { LockGuard } from "../guards/lock.guard";
import { UnauthGuard } from "../guards/unauth.guard";
import { BroadcasterService } from "./broadcaster.service";
import { LockGuardService } from "./lock-guard.service";
import { ModalService } from "./modal.service";
import { PasswordRepromptService } from "./passwordReprompt.service";
import { UnauthGuardService } from "./unauth-guard.service";
import { ValidationService } from "./validation.service";
export const WINDOW = new InjectionToken<Window>("WINDOW");
export const SECURE_STORAGE = new InjectionToken<StorageServiceAbstraction>("SECURE_STORAGE");
export const STATE_FACTORY = new InjectionToken<StateFactory>("STATE_FACTORY");
export const STATE_SERVICE_USE_CACHE = new InjectionToken<boolean>("STATE_SERVICE_USE_CACHE");
export const LOGOUT_CALLBACK = new InjectionToken<(expired: boolean, userId?: string) => void>(
"LOGOUT_CALLBACK"
);
export const LOCKED_CALLBACK = new InjectionToken<() => void>("LOCKED_CALLBACK");
export const CLIENT_TYPE = new InjectionToken<boolean>("CLIENT_TYPE");
export const LOCALES_DIRECTORY = new InjectionToken<string>("LOCALES_DIRECTORY");
export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
@NgModule({
declarations: [],
providers: [
{ provide: "WINDOW", useValue: window },
ValidationService,
AuthGuard,
UnauthGuard,
LockGuard,
ModalService,
{ provide: WINDOW, useValue: window },
{
provide: LOCALE_ID,
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
deps: [I18nServiceAbstraction],
},
ValidationService,
AuthGuardService,
UnauthGuardService,
LockGuardService,
ModalService,
{
provide: LOCALES_DIRECTORY,
useValue: "./locales",
},
{
provide: SYSTEM_LANGUAGE,
useFactory: (window: Window) => window.navigator.language,
deps: [WINDOW],
},
{
provide: STATE_FACTORY,
useValue: new StateFactory(GlobalState, Account),
},
{
provide: STATE_SERVICE_USE_CACHE,
useValue: true,
},
{
provide: LOGOUT_CALLBACK,
useFactory:
(messagingService: MessagingServiceAbstraction) => (expired: boolean, userId?: string) =>
messagingService.send("logout", { expired: expired, userId: userId }),
deps: [MessagingServiceAbstraction],
},
{
provide: LOCKED_CALLBACK,
useValue: null,
},
{
provide: AppIdServiceAbstraction,
useClass: AppIdService,
@@ -203,30 +244,17 @@ import { ValidationService } from "./validation.service";
{
provide: UsernameGenerationServiceAbstraction,
useClass: UsernameGenerationService,
deps: [CryptoServiceAbstraction, StateServiceAbstraction],
deps: [CryptoServiceAbstraction, StateServiceAbstraction, ApiServiceAbstraction],
},
{
provide: ApiServiceAbstraction,
useFactory: (
tokenService: TokenServiceAbstraction,
platformUtilsService: PlatformUtilsServiceAbstraction,
environmentService: EnvironmentServiceAbstraction,
messagingService: MessagingServiceAbstraction,
appIdService: AppIdServiceAbstraction
) =>
new ApiService(
tokenService,
platformUtilsService,
environmentService,
appIdService,
async (expired: boolean) => messagingService.send("logout", { expired: expired })
),
useClass: ApiService,
deps: [
TokenServiceAbstraction,
PlatformUtilsServiceAbstraction,
EnvironmentServiceAbstraction,
MessagingServiceAbstraction,
AppIdServiceAbstraction,
LOGOUT_CALLBACK,
],
},
{
@@ -236,39 +264,7 @@ import { ValidationService } from "./validation.service";
},
{
provide: SyncServiceAbstraction,
useFactory: (
apiService: ApiServiceAbstraction,
settingsService: SettingsServiceAbstraction,
folderService: FolderServiceAbstraction,
cipherService: CipherServiceAbstraction,
cryptoService: CryptoServiceAbstraction,
collectionService: CollectionServiceAbstraction,
messagingService: MessagingServiceAbstraction,
policyService: PolicyServiceAbstraction,
sendService: SendServiceAbstraction,
logService: LogService,
keyConnectorService: KeyConnectorServiceAbstraction,
stateService: StateServiceAbstraction,
organizationService: OrganizationServiceAbstraction,
providerService: ProviderServiceAbstraction
) =>
new SyncService(
apiService,
settingsService,
folderService,
cipherService,
cryptoService,
collectionService,
messagingService,
policyService,
sendService,
logService,
keyConnectorService,
stateService,
organizationService,
providerService,
async (expired: boolean) => messagingService.send("logout", { expired: expired })
),
useClass: SyncService,
deps: [
ApiServiceAbstraction,
SettingsServiceAbstraction,
@@ -284,6 +280,7 @@ import { ValidationService } from "./validation.service";
StateServiceAbstraction,
OrganizationServiceAbstraction,
ProviderServiceAbstraction,
LOGOUT_CALLBACK,
],
},
{ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService },
@@ -294,35 +291,7 @@ import { ValidationService } from "./validation.service";
},
{
provide: VaultTimeoutServiceAbstraction,
useFactory: (
cipherService: CipherServiceAbstraction,
folderService: FolderServiceAbstraction,
collectionService: CollectionServiceAbstraction,
cryptoService: CryptoServiceAbstraction,
platformUtilsService: PlatformUtilsServiceAbstraction,
messagingService: MessagingServiceAbstraction,
searchService: SearchServiceAbstraction,
tokenService: TokenServiceAbstraction,
policyService: PolicyServiceAbstraction,
keyConnectorService: KeyConnectorServiceAbstraction,
stateService: StateServiceAbstraction
) =>
new VaultTimeoutService(
cipherService,
folderService,
collectionService,
cryptoService,
platformUtilsService,
messagingService,
searchService,
tokenService,
policyService,
keyConnectorService,
stateService,
null,
async (userId?: string) =>
messagingService.send("logout", { expired: false, userId: userId })
),
useClass: VaultTimeoutService,
deps: [
CipherServiceAbstraction,
FolderServiceAbstraction,
@@ -335,42 +304,27 @@ import { ValidationService } from "./validation.service";
PolicyServiceAbstraction,
KeyConnectorServiceAbstraction,
StateServiceAbstraction,
AuthServiceAbstraction,
LOCKED_CALLBACK,
LOGOUT_CALLBACK,
],
},
{
provide: StateServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
secureStorageService: StorageServiceAbstraction,
logService: LogService,
stateMigrationService: StateMigrationServiceAbstraction
) =>
new StateService(
storageService,
secureStorageService,
logService,
stateMigrationService,
new StateFactory(GlobalState, Account)
),
useClass: StateService,
deps: [
StorageServiceAbstraction,
"SECURE_STORAGE",
SECURE_STORAGE,
LogService,
StateMigrationServiceAbstraction,
STATE_FACTORY,
STATE_SERVICE_USE_CACHE,
],
},
{
provide: StateMigrationServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
secureStorageService: StorageServiceAbstraction
) =>
new StateMigrationService(
storageService,
secureStorageService,
new StateFactory(GlobalState, Account)
),
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
useClass: StateMigrationService,
deps: [StorageServiceAbstraction, SECURE_STORAGE, STATE_FACTORY],
},
{
provide: ExportServiceAbstraction,
@@ -389,41 +343,22 @@ import { ValidationService } from "./validation.service";
},
{
provide: NotificationsServiceAbstraction,
useFactory: (
syncService: SyncServiceAbstraction,
appIdService: AppIdServiceAbstraction,
apiService: ApiServiceAbstraction,
vaultTimeoutService: VaultTimeoutServiceAbstraction,
environmentService: EnvironmentServiceAbstraction,
messagingService: MessagingServiceAbstraction,
logService: LogService,
stateService: StateServiceAbstraction
) =>
new NotificationsService(
syncService,
appIdService,
apiService,
vaultTimeoutService,
environmentService,
async () => messagingService.send("logout", { expired: true }),
logService,
stateService
),
useClass: NotificationsService,
deps: [
SyncServiceAbstraction,
AppIdServiceAbstraction,
ApiServiceAbstraction,
VaultTimeoutServiceAbstraction,
EnvironmentServiceAbstraction,
MessagingServiceAbstraction,
LOGOUT_CALLBACK,
LogService,
StateServiceAbstraction,
AuthServiceAbstraction,
],
},
{
provide: CryptoFunctionServiceAbstraction,
useClass: WebCryptoFunctionService,
deps: ["WINDOW"],
deps: [WINDOW],
},
{
provide: EventServiceAbstraction,

View File

@@ -1,29 +0,0 @@
import { Injectable } from "@angular/core";
import { CanActivate, Router } from "@angular/router";
import { StateService } from "jslib-common/abstractions/state.service";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
@Injectable()
export class LockGuardService implements CanActivate {
protected homepage = "vault";
protected loginpage = "login";
constructor(
private vaultTimeoutService: VaultTimeoutService,
private router: Router,
private stateService: StateService
) {}
async canActivate() {
if (await this.vaultTimeoutService.isLocked()) {
return true;
}
const redirectUrl = (await this.stateService.getIsAuthenticated())
? [this.homepage]
: [this.loginpage];
this.router.navigate(redirectUrl);
return false;
}
}

View File

@@ -7,6 +7,10 @@ import { PasswordRepromptComponent } from "../components/password-reprompt.compo
import { ModalService } from "./modal.service";
/**
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
* See UserVerificationService for any other situation where you need to verify the user's identity.
*/
@Injectable()
export class PasswordRepromptService implements PasswordRepromptServiceAbstraction {
protected component = PasswordRepromptComponent;

View File

@@ -1,29 +0,0 @@
import { Injectable } from "@angular/core";
import { CanActivate, Router } from "@angular/router";
import { StateService } from "jslib-common/abstractions/state.service";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
@Injectable()
export class UnauthGuardService implements CanActivate {
protected homepage = "vault";
constructor(
private vaultTimeoutService: VaultTimeoutService,
private router: Router,
private stateService: StateService
) {}
async canActivate() {
const isAuthed = await this.stateService.getIsAuthenticated();
if (isAuthed) {
const locked = await this.vaultTimeoutService.isLocked();
if (locked) {
this.router.navigate(["lock"]);
} else {
this.router.navigate([this.homepage]);
}
return false;
}
return true;
}
}

View File

@@ -0,0 +1,21 @@
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from "@angular/forms";
export function notAllowedValueAsync(
valueGetter: () => Promise<string>,
caseInsensitive = false
): AsyncValidatorFn {
return async (control: AbstractControl): Promise<ValidationErrors | null> => {
let notAllowedValue = await valueGetter();
let controlValue = control.value;
if (caseInsensitive) {
notAllowedValue = notAllowedValue.toLowerCase();
controlValue = controlValue.toLowerCase();
}
if (controlValue === notAllowedValue) {
return {
notAllowedValue: true,
};
}
};
}

View File

@@ -1,4 +1,4 @@
const { pathsToModuleNameMapper } = require("ts-jest/utils");
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
@@ -7,7 +7,6 @@ module.exports = {
displayName: "common jslib tests",
preset: "ts-jest",
testEnvironment: "jsdom",
roots: ["<rootDir>/spec/"],
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
collectCoverage: true,

View File

@@ -0,0 +1,83 @@
import Substitute, { Arg } from "@fluffy-spoon/substitute";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { AttachmentData } from "jslib-common/models/data/attachmentData";
import { Attachment } from "jslib-common/models/domain/attachment";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { ContainerService } from "jslib-common/services/container.service";
import { makeStaticByteArray, mockEnc } from "../utils";
describe("Attachment", () => {
let data: AttachmentData;
beforeEach(() => {
data = {
id: "id",
url: "url",
fileName: "fileName",
key: "key",
size: "1100",
sizeName: "1.1 KB",
};
});
it("Convert from empty", () => {
const data = new AttachmentData();
const attachment = new Attachment(data);
expect(attachment).toEqual({
id: null,
url: null,
size: undefined,
sizeName: null,
key: null,
fileName: null,
});
});
it("Convert", () => {
const attachment = new Attachment(data);
expect(attachment).toEqual({
size: "1100",
id: "id",
url: "url",
sizeName: "1.1 KB",
fileName: { encryptedString: "fileName", encryptionType: 0 },
key: { encryptedString: "key", encryptionType: 0 },
});
});
it("toAttachmentData", () => {
const attachment = new Attachment(data);
expect(attachment.toAttachmentData()).toEqual(data);
});
it("Decrypt", async () => {
const attachment = new Attachment();
attachment.id = "id";
attachment.url = "url";
attachment.size = "1100";
attachment.sizeName = "1.1 KB";
attachment.key = mockEnc("key");
attachment.fileName = mockEnc("fileName");
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
cryptoService.decryptToBytes(Arg.any(), Arg.any()).resolves(makeStaticByteArray(32));
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
const view = await attachment.decrypt(null);
expect(view).toEqual({
id: "id",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "fileName",
key: expect.any(SymmetricCryptoKey),
});
});
});

View File

@@ -0,0 +1,73 @@
import { CardData } from "jslib-common/models/data/cardData";
import { Card } from "jslib-common/models/domain/card";
import { mockEnc } from "../utils";
describe("Card", () => {
let data: CardData;
beforeEach(() => {
data = {
cardholderName: "encHolder",
brand: "encBrand",
number: "encNumber",
expMonth: "encMonth",
expYear: "encYear",
code: "encCode",
};
});
it("Convert from empty", () => {
const data = new CardData();
const card = new Card(data);
expect(card).toEqual({
cardholderName: null,
brand: null,
number: null,
expMonth: null,
expYear: null,
code: null,
});
});
it("Convert", () => {
const card = new Card(data);
expect(card).toEqual({
cardholderName: { encryptedString: "encHolder", encryptionType: 0 },
brand: { encryptedString: "encBrand", encryptionType: 0 },
number: { encryptedString: "encNumber", encryptionType: 0 },
expMonth: { encryptedString: "encMonth", encryptionType: 0 },
expYear: { encryptedString: "encYear", encryptionType: 0 },
code: { encryptedString: "encCode", encryptionType: 0 },
});
});
it("toCardData", () => {
const card = new Card(data);
expect(card.toCardData()).toEqual(data);
});
it("Decrypt", async () => {
const card = new Card();
card.cardholderName = mockEnc("cardHolder");
card.brand = mockEnc("brand");
card.number = mockEnc("number");
card.expMonth = mockEnc("expMonth");
card.expYear = mockEnc("expYear");
card.code = mockEnc("code");
const view = await card.decrypt(null);
expect(view).toEqual({
_brand: "brand",
_number: "number",
_subTitle: null,
cardholderName: "cardHolder",
code: "code",
expMonth: "expMonth",
expYear: "expYear",
});
});
});

View File

@@ -0,0 +1,590 @@
import Substitute, { Arg } from "@fluffy-spoon/substitute";
import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
import { CipherType } from "jslib-common/enums/cipherType";
import { FieldType } from "jslib-common/enums/fieldType";
import { SecureNoteType } from "jslib-common/enums/secureNoteType";
import { UriMatchType } from "jslib-common/enums/uriMatchType";
import { CipherData } from "jslib-common/models/data/cipherData";
import { Card } from "jslib-common/models/domain/card";
import { Cipher } from "jslib-common/models/domain/cipher";
import { Identity } from "jslib-common/models/domain/identity";
import { Login } from "jslib-common/models/domain/login";
import { SecureNote } from "jslib-common/models/domain/secureNote";
import { CardView } from "jslib-common/models/view/cardView";
import { IdentityView } from "jslib-common/models/view/identityView";
import { LoginView } from "jslib-common/models/view/loginView";
import { mockEnc } from "../utils";
describe("Cipher DTO", () => {
it("Convert from empty CipherData", () => {
const data = new CipherData();
const cipher = new Cipher(data);
expect(cipher).toEqual({
id: null,
organizationId: null,
folderId: null,
name: null,
notes: null,
type: undefined,
favorite: undefined,
organizationUseTotp: undefined,
edit: undefined,
viewPassword: true,
revisionDate: null,
collectionIds: undefined,
localData: null,
deletedDate: null,
reprompt: undefined,
attachments: null,
fields: null,
passwordHistory: null,
});
});
describe("LoginCipher", () => {
let cipherData: CipherData;
beforeEach(() => {
cipherData = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
edit: true,
viewPassword: true,
organizationUseTotp: true,
favorite: false,
revisionDate: "2022-01-31T12:00:00.000Z",
type: CipherType.Login,
name: "EncryptedString",
notes: "EncryptedString",
deletedDate: null,
reprompt: CipherRepromptType.None,
login: {
uris: [{ uri: "EncryptedString", match: UriMatchType.Domain }],
username: "EncryptedString",
password: "EncryptedString",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
totp: "EncryptedString",
autofillOnPageLoad: false,
},
passwordHistory: [
{ password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" },
],
attachments: [
{
id: "a1",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "file",
key: "EncKey",
},
{
id: "a2",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "file",
key: "EncKey",
},
],
fields: [
{
name: "EncryptedString",
value: "EncryptedString",
type: FieldType.Text,
linkedId: null,
},
{
name: "EncryptedString",
value: "EncryptedString",
type: FieldType.Hidden,
linkedId: null,
},
],
};
});
it("Convert", () => {
const cipher = new Cipher(cipherData);
expect(cipher).toEqual({
id: "id",
organizationId: "orgId",
folderId: "folderId",
name: { encryptedString: "EncryptedString", encryptionType: 0 },
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
type: 1,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
collectionIds: undefined,
localData: null,
deletedDate: null,
reprompt: 0,
login: {
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
autofillOnPageLoad: false,
username: { encryptedString: "EncryptedString", encryptionType: 0 },
password: { encryptedString: "EncryptedString", encryptionType: 0 },
totp: { encryptedString: "EncryptedString", encryptionType: 0 },
uris: [{ match: 0, uri: { encryptedString: "EncryptedString", encryptionType: 0 } }],
},
attachments: [
{
fileName: { encryptedString: "file", encryptionType: 0 },
id: "a1",
key: { encryptedString: "EncKey", encryptionType: 0 },
size: "1100",
sizeName: "1.1 KB",
url: "url",
},
{
fileName: { encryptedString: "file", encryptionType: 0 },
id: "a2",
key: { encryptedString: "EncKey", encryptionType: 0 },
size: "1100",
sizeName: "1.1 KB",
url: "url",
},
],
fields: [
{
linkedId: null,
name: { encryptedString: "EncryptedString", encryptionType: 0 },
type: 0,
value: { encryptedString: "EncryptedString", encryptionType: 0 },
},
{
linkedId: null,
name: { encryptedString: "EncryptedString", encryptionType: 0 },
type: 1,
value: { encryptedString: "EncryptedString", encryptionType: 0 },
},
],
passwordHistory: [
{
lastUsedDate: new Date("2022-01-31T12:00:00.000Z"),
password: { encryptedString: "EncryptedString", encryptionType: 0 },
},
],
});
});
it("toCipherData", () => {
const cipher = new Cipher(cipherData);
expect(cipher.toCipherData()).toEqual(cipherData);
});
it("Decrypt", async () => {
const cipher = new Cipher();
cipher.id = "id";
cipher.organizationId = "orgId";
cipher.folderId = "folderId";
cipher.edit = true;
cipher.viewPassword = true;
cipher.organizationUseTotp = true;
cipher.favorite = false;
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
cipher.type = CipherType.Login;
cipher.name = mockEnc("EncryptedString");
cipher.notes = mockEnc("EncryptedString");
cipher.deletedDate = null;
cipher.reprompt = CipherRepromptType.None;
const loginView = new LoginView();
loginView.username = "username";
loginView.password = "password";
const login = Substitute.for<Login>();
login.decrypt(Arg.any(), Arg.any()).resolves(loginView);
cipher.login = login;
const cipherView = await cipher.decrypt();
expect(cipherView).toMatchObject({
id: "id",
organizationId: "orgId",
folderId: "folderId",
name: "EncryptedString",
notes: "EncryptedString",
type: 1,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
login: loginView,
attachments: null,
fields: null,
passwordHistory: null,
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
deletedDate: null,
reprompt: 0,
localData: undefined,
});
});
});
describe("SecureNoteCipher", () => {
let cipherData: CipherData;
beforeEach(() => {
cipherData = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
edit: true,
viewPassword: true,
organizationUseTotp: true,
favorite: false,
revisionDate: "2022-01-31T12:00:00.000Z",
type: CipherType.SecureNote,
name: "EncryptedString",
notes: "EncryptedString",
deletedDate: null,
reprompt: CipherRepromptType.None,
secureNote: {
type: SecureNoteType.Generic,
},
};
});
it("Convert", () => {
const cipher = new Cipher(cipherData);
expect(cipher).toEqual({
id: "id",
organizationId: "orgId",
folderId: "folderId",
name: { encryptedString: "EncryptedString", encryptionType: 0 },
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
type: 2,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
collectionIds: undefined,
localData: null,
deletedDate: null,
reprompt: 0,
secureNote: { type: SecureNoteType.Generic },
attachments: null,
fields: null,
passwordHistory: null,
});
});
it("toCipherData", () => {
const cipher = new Cipher(cipherData);
expect(cipher.toCipherData()).toEqual(cipherData);
});
it("Decrypt", async () => {
const cipher = new Cipher();
cipher.id = "id";
cipher.organizationId = "orgId";
cipher.folderId = "folderId";
cipher.edit = true;
cipher.viewPassword = true;
cipher.organizationUseTotp = true;
cipher.favorite = false;
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
cipher.type = CipherType.SecureNote;
cipher.name = mockEnc("EncryptedString");
cipher.notes = mockEnc("EncryptedString");
cipher.deletedDate = null;
cipher.reprompt = CipherRepromptType.None;
cipher.secureNote = new SecureNote();
cipher.secureNote.type = SecureNoteType.Generic;
const cipherView = await cipher.decrypt();
expect(cipherView).toMatchObject({
id: "id",
organizationId: "orgId",
folderId: "folderId",
name: "EncryptedString",
notes: "EncryptedString",
type: 2,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
secureNote: { type: 0 },
attachments: null,
fields: null,
passwordHistory: null,
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
deletedDate: null,
reprompt: 0,
localData: undefined,
});
});
});
describe("CardCipher", () => {
let cipherData: CipherData;
beforeEach(() => {
cipherData = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
edit: true,
viewPassword: true,
organizationUseTotp: true,
favorite: false,
revisionDate: "2022-01-31T12:00:00.000Z",
type: CipherType.Card,
name: "EncryptedString",
notes: "EncryptedString",
deletedDate: null,
reprompt: CipherRepromptType.None,
card: {
cardholderName: "EncryptedString",
brand: "EncryptedString",
number: "EncryptedString",
expMonth: "EncryptedString",
expYear: "EncryptedString",
code: "EncryptedString",
},
};
});
it("Convert", () => {
const cipher = new Cipher(cipherData);
expect(cipher).toEqual({
id: "id",
organizationId: "orgId",
folderId: "folderId",
name: { encryptedString: "EncryptedString", encryptionType: 0 },
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
type: 3,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
collectionIds: undefined,
localData: null,
deletedDate: null,
reprompt: 0,
card: {
cardholderName: { encryptedString: "EncryptedString", encryptionType: 0 },
brand: { encryptedString: "EncryptedString", encryptionType: 0 },
number: { encryptedString: "EncryptedString", encryptionType: 0 },
expMonth: { encryptedString: "EncryptedString", encryptionType: 0 },
expYear: { encryptedString: "EncryptedString", encryptionType: 0 },
code: { encryptedString: "EncryptedString", encryptionType: 0 },
},
attachments: null,
fields: null,
passwordHistory: null,
});
});
it("toCipherData", () => {
const cipher = new Cipher(cipherData);
expect(cipher.toCipherData()).toEqual(cipherData);
});
it("Decrypt", async () => {
const cipher = new Cipher();
cipher.id = "id";
cipher.organizationId = "orgId";
cipher.folderId = "folderId";
cipher.edit = true;
cipher.viewPassword = true;
cipher.organizationUseTotp = true;
cipher.favorite = false;
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
cipher.type = CipherType.Card;
cipher.name = mockEnc("EncryptedString");
cipher.notes = mockEnc("EncryptedString");
cipher.deletedDate = null;
cipher.reprompt = CipherRepromptType.None;
const cardView = new CardView();
cardView.cardholderName = "cardholderName";
cardView.number = "4111111111111111";
const card = Substitute.for<Card>();
card.decrypt(Arg.any(), Arg.any()).resolves(cardView);
cipher.card = card;
const cipherView = await cipher.decrypt();
expect(cipherView).toMatchObject({
id: "id",
organizationId: "orgId",
folderId: "folderId",
name: "EncryptedString",
notes: "EncryptedString",
type: 3,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
card: cardView,
attachments: null,
fields: null,
passwordHistory: null,
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
deletedDate: null,
reprompt: 0,
localData: undefined,
});
});
});
describe("IdentityCipher", () => {
let cipherData: CipherData;
beforeEach(() => {
cipherData = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
edit: true,
viewPassword: true,
organizationUseTotp: true,
favorite: false,
revisionDate: "2022-01-31T12:00:00.000Z",
type: CipherType.Identity,
name: "EncryptedString",
notes: "EncryptedString",
deletedDate: null,
reprompt: CipherRepromptType.None,
identity: {
title: "EncryptedString",
firstName: "EncryptedString",
middleName: "EncryptedString",
lastName: "EncryptedString",
address1: "EncryptedString",
address2: "EncryptedString",
address3: "EncryptedString",
city: "EncryptedString",
state: "EncryptedString",
postalCode: "EncryptedString",
country: "EncryptedString",
company: "EncryptedString",
email: "EncryptedString",
phone: "EncryptedString",
ssn: "EncryptedString",
username: "EncryptedString",
passportNumber: "EncryptedString",
licenseNumber: "EncryptedString",
},
};
});
it("Convert", () => {
const cipher = new Cipher(cipherData);
expect(cipher).toEqual({
id: "id",
organizationId: "orgId",
folderId: "folderId",
name: { encryptedString: "EncryptedString", encryptionType: 0 },
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
type: 4,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
collectionIds: undefined,
localData: null,
deletedDate: null,
reprompt: 0,
identity: {
title: { encryptedString: "EncryptedString", encryptionType: 0 },
firstName: { encryptedString: "EncryptedString", encryptionType: 0 },
middleName: { encryptedString: "EncryptedString", encryptionType: 0 },
lastName: { encryptedString: "EncryptedString", encryptionType: 0 },
address1: { encryptedString: "EncryptedString", encryptionType: 0 },
address2: { encryptedString: "EncryptedString", encryptionType: 0 },
address3: { encryptedString: "EncryptedString", encryptionType: 0 },
city: { encryptedString: "EncryptedString", encryptionType: 0 },
state: { encryptedString: "EncryptedString", encryptionType: 0 },
postalCode: { encryptedString: "EncryptedString", encryptionType: 0 },
country: { encryptedString: "EncryptedString", encryptionType: 0 },
company: { encryptedString: "EncryptedString", encryptionType: 0 },
email: { encryptedString: "EncryptedString", encryptionType: 0 },
phone: { encryptedString: "EncryptedString", encryptionType: 0 },
ssn: { encryptedString: "EncryptedString", encryptionType: 0 },
username: { encryptedString: "EncryptedString", encryptionType: 0 },
passportNumber: { encryptedString: "EncryptedString", encryptionType: 0 },
licenseNumber: { encryptedString: "EncryptedString", encryptionType: 0 },
},
attachments: null,
fields: null,
passwordHistory: null,
});
});
it("toCipherData", () => {
const cipher = new Cipher(cipherData);
expect(cipher.toCipherData()).toEqual(cipherData);
});
it("Decrypt", async () => {
const cipher = new Cipher();
cipher.id = "id";
cipher.organizationId = "orgId";
cipher.folderId = "folderId";
cipher.edit = true;
cipher.viewPassword = true;
cipher.organizationUseTotp = true;
cipher.favorite = false;
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
cipher.type = CipherType.Identity;
cipher.name = mockEnc("EncryptedString");
cipher.notes = mockEnc("EncryptedString");
cipher.deletedDate = null;
cipher.reprompt = CipherRepromptType.None;
const identityView = new IdentityView();
identityView.firstName = "firstName";
identityView.lastName = "lastName";
const identity = Substitute.for<Identity>();
identity.decrypt(Arg.any(), Arg.any()).resolves(identityView);
cipher.identity = identity;
const cipherView = await cipher.decrypt();
expect(cipherView).toMatchObject({
id: "id",
organizationId: "orgId",
folderId: "folderId",
name: "EncryptedString",
notes: "EncryptedString",
type: 4,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
identity: identityView,
attachments: null,
fields: null,
passwordHistory: null,
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
deletedDate: null,
reprompt: 0,
localData: undefined,
});
});
});
});

View File

@@ -0,0 +1,66 @@
import { CollectionData } from "jslib-common/models/data/collectionData";
import { Collection } from "jslib-common/models/domain/collection";
import { mockEnc } from "../utils";
describe("Collection", () => {
let data: CollectionData;
beforeEach(() => {
data = {
id: "id",
organizationId: "orgId",
name: "encName",
externalId: "extId",
readOnly: true,
};
});
it("Convert from empty", () => {
const data = new CollectionData({} as any);
const card = new Collection(data);
expect(card).toEqual({
externalId: null,
hidePasswords: null,
id: null,
name: null,
organizationId: null,
readOnly: null,
});
});
it("Convert", () => {
const collection = new Collection(data);
expect(collection).toEqual({
id: "id",
organizationId: "orgId",
name: { encryptedString: "encName", encryptionType: 0 },
externalId: "extId",
readOnly: true,
hidePasswords: null,
});
});
it("Decrypt", async () => {
const collection = new Collection();
collection.id = "id";
collection.organizationId = "orgId";
collection.name = mockEnc("encName");
collection.externalId = "extId";
collection.readOnly = false;
collection.hidePasswords = false;
const view = await collection.decrypt();
expect(view).toEqual({
externalId: "extId",
hidePasswords: false,
id: "id",
name: "encName",
organizationId: "orgId",
readOnly: false,
});
});
});

View File

@@ -0,0 +1,195 @@
import Substitute, { Arg } from "@fluffy-spoon/substitute";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EncryptionType } from "jslib-common/enums/encryptionType";
import { EncString } from "jslib-common/models/domain/encString";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { ContainerService } from "jslib-common/services/container.service";
describe("EncString", () => {
afterEach(() => {
(window as any).bitwardenContainerService = undefined;
});
describe("Rsa2048_OaepSha256_B64", () => {
it("constructor", () => {
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
expect(encString).toEqual({
data: "data",
encryptedString: "3.data",
encryptionType: 3,
});
});
describe("parse existing", () => {
it("valid", () => {
const encString = new EncString("3.data");
expect(encString).toEqual({
data: "data",
encryptedString: "3.data",
encryptionType: 3,
});
});
it("invalid", () => {
const encString = new EncString("3.data|test");
expect(encString).toEqual({
encryptedString: "3.data|test",
encryptionType: 3,
});
});
});
describe("decrypt", () => {
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
cryptoService.decryptToUtf8(encString, Arg.any()).resolves("decrypted");
beforeEach(() => {
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
});
it("decrypts correctly", async () => {
const decrypted = await encString.decrypt(null);
expect(decrypted).toBe("decrypted");
});
it("result should be cached", async () => {
const decrypted = await encString.decrypt(null);
cryptoService.received(1).decryptToUtf8(Arg.any(), Arg.any());
expect(decrypted).toBe("decrypted");
});
});
});
describe("AesCbc256_B64", () => {
it("constructor", () => {
const encString = new EncString(EncryptionType.AesCbc256_B64, "data", "iv");
expect(encString).toEqual({
data: "data",
encryptedString: "0.iv|data",
encryptionType: 0,
iv: "iv",
});
});
describe("parse existing", () => {
it("valid", () => {
const encString = new EncString("0.iv|data");
expect(encString).toEqual({
data: "data",
encryptedString: "0.iv|data",
encryptionType: 0,
iv: "iv",
});
});
it("invalid", () => {
const encString = new EncString("0.iv|data|mac");
expect(encString).toEqual({
encryptedString: "0.iv|data|mac",
encryptionType: 0,
});
});
});
});
describe("AesCbc256_HmacSha256_B64", () => {
it("constructor", () => {
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
expect(encString).toEqual({
data: "data",
encryptedString: "2.iv|data|mac",
encryptionType: 2,
iv: "iv",
mac: "mac",
});
});
it("valid", () => {
const encString = new EncString("2.iv|data|mac");
expect(encString).toEqual({
data: "data",
encryptedString: "2.iv|data|mac",
encryptionType: 2,
iv: "iv",
mac: "mac",
});
});
it("invalid", () => {
const encString = new EncString("2.iv|data");
expect(encString).toEqual({
encryptedString: "2.iv|data",
encryptionType: 2,
});
});
});
it("Exit early if null", () => {
const encString = new EncString(null);
expect(encString).toEqual({
encryptedString: null,
});
});
describe("decrypt", () => {
it("throws exception when bitwarden container not initialized", async () => {
const encString = new EncString(null);
expect.assertions(1);
try {
await encString.decrypt(null);
} catch (e) {
expect(e.message).toEqual("global bitwardenContainerService not initialized.");
}
});
it("handles value it can't decrypt", async () => {
const encString = new EncString(null);
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
cryptoService.decryptToUtf8(encString, Arg.any()).throws("error");
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
const decrypted = await encString.decrypt(null);
expect(decrypted).toBe("[error: cannot decrypt]");
expect(encString).toEqual({
decryptedValue: "[error: cannot decrypt]",
encryptedString: null,
});
});
it("passes along key", async () => {
const encString = new EncString(null);
const key = Substitute.for<SymmetricCryptoKey>();
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
await encString.decrypt(null, key);
cryptoService.received().decryptToUtf8(encString, key);
});
});
});

View File

@@ -0,0 +1,64 @@
import { FieldType } from "jslib-common/enums/fieldType";
import { FieldData } from "jslib-common/models/data/fieldData";
import { Field } from "jslib-common/models/domain/field";
import { mockEnc } from "../utils";
describe("Field", () => {
let data: FieldData;
beforeEach(() => {
data = {
type: FieldType.Text,
name: "encName",
value: "encValue",
linkedId: null,
};
});
it("Convert from empty", () => {
const data = new FieldData();
const field = new Field(data);
expect(field).toEqual({
type: undefined,
name: null,
value: null,
linkedId: undefined,
});
});
it("Convert", () => {
const field = new Field(data);
expect(field).toEqual({
type: FieldType.Text,
name: { encryptedString: "encName", encryptionType: 0 },
value: { encryptedString: "encValue", encryptionType: 0 },
linkedId: null,
});
});
it("toFieldData", () => {
const field = new Field(data);
expect(field.toFieldData()).toEqual(data);
});
it("Decrypt", async () => {
const field = new Field();
field.type = FieldType.Text;
field.name = mockEnc("encName");
field.value = mockEnc("encValue");
const view = await field.decrypt(null);
expect(view).toEqual({
type: 0,
name: "encName",
value: "encValue",
newField: false,
showCount: false,
showValue: false,
});
});
});

View File

@@ -0,0 +1,41 @@
import { FolderData } from "jslib-common/models/data/folderData";
import { Folder } from "jslib-common/models/domain/folder";
import { mockEnc } from "../utils";
describe("Folder", () => {
let data: FolderData;
beforeEach(() => {
data = {
id: "id",
name: "encName",
revisionDate: "2022-01-31T12:00:00.000Z",
};
});
it("Convert", () => {
const field = new Folder(data);
expect(field).toEqual({
id: "id",
name: { encryptedString: "encName", encryptionType: 0 },
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
});
});
it("Decrypt", async () => {
const folder = new Folder();
folder.id = "id";
folder.name = mockEnc("encName");
folder.revisionDate = new Date("2022-01-31T12:00:00.000Z");
const view = await folder.decrypt();
expect(view).toEqual({
id: "id",
name: "encName",
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
});
});
});

View File

@@ -0,0 +1,134 @@
import { IdentityData } from "jslib-common/models/data/identityData";
import { Identity } from "jslib-common/models/domain/identity";
import { mockEnc } from "../utils";
describe("Identity", () => {
let data: IdentityData;
beforeEach(() => {
data = {
title: "enctitle",
firstName: "encfirstName",
middleName: "encmiddleName",
lastName: "enclastName",
address1: "encaddress1",
address2: "encaddress2",
address3: "encaddress3",
city: "enccity",
state: "encstate",
postalCode: "encpostalCode",
country: "enccountry",
company: "enccompany",
email: "encemail",
phone: "encphone",
ssn: "encssn",
username: "encusername",
passportNumber: "encpassportNumber",
licenseNumber: "enclicenseNumber",
};
});
it("Convert from empty", () => {
const data = new IdentityData();
const identity = new Identity(data);
expect(identity).toEqual({
address1: null,
address2: null,
address3: null,
city: null,
company: null,
country: null,
email: null,
firstName: null,
lastName: null,
licenseNumber: null,
middleName: null,
passportNumber: null,
phone: null,
postalCode: null,
ssn: null,
state: null,
title: null,
username: null,
});
});
it("Convert", () => {
const identity = new Identity(data);
expect(identity).toEqual({
title: { encryptedString: "enctitle", encryptionType: 0 },
firstName: { encryptedString: "encfirstName", encryptionType: 0 },
middleName: { encryptedString: "encmiddleName", encryptionType: 0 },
lastName: { encryptedString: "enclastName", encryptionType: 0 },
address1: { encryptedString: "encaddress1", encryptionType: 0 },
address2: { encryptedString: "encaddress2", encryptionType: 0 },
address3: { encryptedString: "encaddress3", encryptionType: 0 },
city: { encryptedString: "enccity", encryptionType: 0 },
state: { encryptedString: "encstate", encryptionType: 0 },
postalCode: { encryptedString: "encpostalCode", encryptionType: 0 },
country: { encryptedString: "enccountry", encryptionType: 0 },
company: { encryptedString: "enccompany", encryptionType: 0 },
email: { encryptedString: "encemail", encryptionType: 0 },
phone: { encryptedString: "encphone", encryptionType: 0 },
ssn: { encryptedString: "encssn", encryptionType: 0 },
username: { encryptedString: "encusername", encryptionType: 0 },
passportNumber: { encryptedString: "encpassportNumber", encryptionType: 0 },
licenseNumber: { encryptedString: "enclicenseNumber", encryptionType: 0 },
});
});
it("toIdentityData", () => {
const identity = new Identity(data);
expect(identity.toIdentityData()).toEqual(data);
});
it("Decrypt", async () => {
const identity = new Identity();
identity.title = mockEnc("mockTitle");
identity.firstName = mockEnc("mockFirstName");
identity.middleName = mockEnc("mockMiddleName");
identity.lastName = mockEnc("mockLastName");
identity.address1 = mockEnc("mockAddress1");
identity.address2 = mockEnc("mockAddress2");
identity.address3 = mockEnc("mockAddress3");
identity.city = mockEnc("mockCity");
identity.state = mockEnc("mockState");
identity.postalCode = mockEnc("mockPostalCode");
identity.country = mockEnc("mockCountry");
identity.company = mockEnc("mockCompany");
identity.email = mockEnc("mockEmail");
identity.phone = mockEnc("mockPhone");
identity.ssn = mockEnc("mockSsn");
identity.username = mockEnc("mockUsername");
identity.passportNumber = mockEnc("mockPassportNumber");
identity.licenseNumber = mockEnc("mockLicenseNumber");
const view = await identity.decrypt(null);
expect(view).toEqual({
_firstName: "mockFirstName",
_lastName: "mockLastName",
_subTitle: null,
address1: "mockAddress1",
address2: "mockAddress2",
address3: "mockAddress3",
city: "mockCity",
company: "mockCompany",
country: "mockCountry",
email: "mockEmail",
licenseNumber: "mockLicenseNumber",
middleName: "mockMiddleName",
passportNumber: "mockPassportNumber",
phone: "mockPhone",
postalCode: "mockPostalCode",
ssn: "mockSsn",
state: "mockState",
title: "mockTitle",
username: "mockUsername",
});
});
});

View File

@@ -0,0 +1,101 @@
import Substitute, { Arg } from "@fluffy-spoon/substitute";
import { UriMatchType } from "jslib-common/enums/uriMatchType";
import { LoginData } from "jslib-common/models/data/loginData";
import { Login } from "jslib-common/models/domain/login";
import { LoginUri } from "jslib-common/models/domain/loginUri";
import { LoginUriView } from "jslib-common/models/view/loginUriView";
import { mockEnc } from "../utils";
describe("Login DTO", () => {
it("Convert from empty LoginData", () => {
const data = new LoginData();
const login = new Login(data);
expect(login).toEqual({
passwordRevisionDate: null,
autofillOnPageLoad: undefined,
username: null,
password: null,
totp: null,
});
});
it("Convert from full LoginData", () => {
const data: LoginData = {
uris: [{ uri: "uri", match: UriMatchType.Domain }],
username: "username",
password: "password",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
totp: "123",
autofillOnPageLoad: false,
};
const login = new Login(data);
expect(login).toEqual({
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
autofillOnPageLoad: false,
username: { encryptedString: "username", encryptionType: 0 },
password: { encryptedString: "password", encryptionType: 0 },
totp: { encryptedString: "123", encryptionType: 0 },
uris: [{ match: 0, uri: { encryptedString: "uri", encryptionType: 0 } }],
});
});
it("Initialize without LoginData", () => {
const login = new Login();
expect(login).toEqual({});
});
it("Decrypts correctly", async () => {
const loginUri = Substitute.for<LoginUri>();
const loginUriView = new LoginUriView();
loginUriView.uri = "decrypted uri";
loginUri.decrypt(Arg.any()).resolves(loginUriView);
const login = new Login();
login.uris = [loginUri];
login.username = mockEnc("encrypted username");
login.password = mockEnc("encrypted password");
login.passwordRevisionDate = new Date("2022-01-31T12:00:00.000Z");
login.totp = mockEnc("encrypted totp");
login.autofillOnPageLoad = true;
const loginView = await login.decrypt(null);
expect(loginView).toEqual({
username: "encrypted username",
password: "encrypted password",
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
totp: "encrypted totp",
uris: [
{
match: null,
_uri: "decrypted uri",
_domain: null,
_hostname: null,
_host: null,
_canLaunch: null,
},
],
autofillOnPageLoad: true,
});
});
it("Converts from LoginData and back", () => {
const data: LoginData = {
uris: [{ uri: "uri", match: UriMatchType.Domain }],
username: "username",
password: "password",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
totp: "123",
autofillOnPageLoad: false,
};
const login = new Login(data);
const loginData = login.toLoginData();
expect(loginData).toEqual(data);
});
});

View File

@@ -0,0 +1,57 @@
import { UriMatchType } from "jslib-common/enums/uriMatchType";
import { LoginUriData } from "jslib-common/models/data/loginUriData";
import { LoginUri } from "jslib-common/models/domain/loginUri";
import { mockEnc } from "../utils";
describe("LoginUri", () => {
let data: LoginUriData;
beforeEach(() => {
data = {
uri: "encUri",
match: UriMatchType.Domain,
};
});
it("Convert from empty", () => {
const data = new LoginUriData();
const loginUri = new LoginUri(data);
expect(loginUri).toEqual({
match: null,
uri: null,
});
});
it("Convert", () => {
const loginUri = new LoginUri(data);
expect(loginUri).toEqual({
match: 0,
uri: { encryptedString: "encUri", encryptionType: 0 },
});
});
it("toLoginUriData", () => {
const loginUri = new LoginUri(data);
expect(loginUri.toLoginUriData()).toEqual(data);
});
it("Decrypt", async () => {
const loginUri = new LoginUri();
loginUri.match = UriMatchType.Exact;
loginUri.uri = mockEnc("uri");
const view = await loginUri.decrypt(null);
expect(view).toEqual({
_canLaunch: null,
_domain: null,
_host: null,
_hostname: null,
_uri: "uri",
match: 3,
});
});
});

View File

@@ -0,0 +1,51 @@
import { PasswordHistoryData } from "jslib-common/models/data/passwordHistoryData";
import { Password } from "jslib-common/models/domain/password";
import { mockEnc } from "../utils";
describe("Password", () => {
let data: PasswordHistoryData;
beforeEach(() => {
data = {
password: "encPassword",
lastUsedDate: "2022-01-31T12:00:00.000Z",
};
});
it("Convert from empty", () => {
const data = new PasswordHistoryData();
const password = new Password(data);
expect(password).toMatchObject({
password: null,
});
});
it("Convert", () => {
const password = new Password(data);
expect(password).toEqual({
password: { encryptedString: "encPassword", encryptionType: 0 },
lastUsedDate: new Date("2022-01-31T12:00:00.000Z"),
});
});
it("toPasswordHistoryData", () => {
const password = new Password(data);
expect(password.toPasswordHistoryData()).toEqual(data);
});
it("Decrypt", async () => {
const password = new Password();
password.password = mockEnc("password");
password.lastUsedDate = new Date("2022-01-31T12:00:00.000Z");
const view = await password.decrypt(null);
expect(view).toEqual({
password: "password",
lastUsedDate: new Date("2022-01-31T12:00:00.000Z"),
});
});
});

View File

@@ -0,0 +1,46 @@
import { SecureNoteType } from "jslib-common/enums/secureNoteType";
import { SecureNoteData } from "jslib-common/models/data/secureNoteData";
import { SecureNote } from "jslib-common/models/domain/secureNote";
describe("SecureNote", () => {
let data: SecureNoteData;
beforeEach(() => {
data = {
type: SecureNoteType.Generic,
};
});
it("Convert from empty", () => {
const data = new SecureNoteData();
const secureNote = new SecureNote(data);
expect(secureNote).toEqual({
type: undefined,
});
});
it("Convert", () => {
const secureNote = new SecureNote(data);
expect(secureNote).toEqual({
type: 0,
});
});
it("toSecureNoteData", () => {
const secureNote = new SecureNote(data);
expect(secureNote.toSecureNoteData()).toEqual(data);
});
it("Decrypt", async () => {
const secureNote = new SecureNote();
secureNote.type = SecureNoteType.Generic;
const view = await secureNote.decrypt(null);
expect(view).toEqual({
type: 0,
});
});
});

View File

@@ -0,0 +1,140 @@
import Substitute, { Arg, SubstituteOf } from "@fluffy-spoon/substitute";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { SendType } from "jslib-common/enums/sendType";
import { SendData } from "jslib-common/models/data/sendData";
import { EncString } from "jslib-common/models/domain/encString";
import { Send } from "jslib-common/models/domain/send";
import { SendText } from "jslib-common/models/domain/sendText";
import { ContainerService } from "jslib-common/services/container.service";
import { makeStaticByteArray, mockEnc } from "../utils";
describe("Send", () => {
let data: SendData;
beforeEach(() => {
data = {
id: "id",
accessId: "accessId",
type: SendType.Text,
name: "encName",
notes: "encNotes",
text: {
text: "encText",
hidden: true,
},
file: null,
key: "encKey",
maxAccessCount: null,
accessCount: 10,
revisionDate: "2022-01-31T12:00:00.000Z",
expirationDate: "2022-01-31T12:00:00.000Z",
deletionDate: "2022-01-31T12:00:00.000Z",
password: "password",
disabled: false,
hideEmail: true,
};
});
it("Convert from empty", () => {
const data = new SendData();
const send = new Send(data);
expect(send).toEqual({
id: null,
accessId: null,
type: undefined,
name: null,
notes: null,
text: undefined,
file: undefined,
key: null,
maxAccessCount: undefined,
accessCount: undefined,
revisionDate: null,
expirationDate: null,
deletionDate: null,
password: undefined,
disabled: undefined,
hideEmail: undefined,
});
});
it("Convert", () => {
const send = new Send(data);
expect(send).toEqual({
id: "id",
accessId: "accessId",
type: SendType.Text,
name: { encryptedString: "encName", encryptionType: 0 },
notes: { encryptedString: "encNotes", encryptionType: 0 },
text: {
text: { encryptedString: "encText", encryptionType: 0 },
hidden: true,
},
key: { encryptedString: "encKey", encryptionType: 0 },
maxAccessCount: null,
accessCount: 10,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
deletionDate: new Date("2022-01-31T12:00:00.000Z"),
password: "password",
disabled: false,
hideEmail: true,
});
});
it("Decrypt", async () => {
const text = Substitute.for<SendText>();
text.decrypt(Arg.any()).resolves("textView" as any);
const send = new Send();
send.id = "id";
send.accessId = "accessId";
send.type = SendType.Text;
send.name = mockEnc("name");
send.notes = mockEnc("notes");
send.text = text;
send.key = mockEnc("key");
send.accessCount = 10;
send.revisionDate = new Date("2022-01-31T12:00:00.000Z");
send.expirationDate = new Date("2022-01-31T12:00:00.000Z");
send.deletionDate = new Date("2022-01-31T12:00:00.000Z");
send.password = "password";
send.disabled = false;
send.hideEmail = true;
const cryptoService = Substitute.for<CryptoService>();
cryptoService.decryptToBytes(send.key, null).resolves(makeStaticByteArray(32));
cryptoService.makeSendKey(Arg.any()).resolves("cryptoKey" as any);
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
const view = await send.decrypt();
text.received(1).decrypt("cryptoKey" as any);
(send.name as SubstituteOf<EncString>).received(1).decrypt(null, "cryptoKey" as any);
expect(view).toMatchObject({
id: "id",
accessId: "accessId",
name: "name",
notes: "notes",
type: 0,
key: expect.anything(),
cryptoKey: "cryptoKey",
file: expect.anything(),
text: "textView",
maxAccessCount: undefined,
accessCount: 10,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
deletionDate: new Date("2022-01-31T12:00:00.000Z"),
password: "password",
disabled: false,
hideEmail: true,
});
});
});

View File

@@ -0,0 +1,84 @@
import Substitute, { Arg } from "@fluffy-spoon/substitute";
import { SendType } from "jslib-common/enums/sendType";
import { SendAccess } from "jslib-common/models/domain/sendAccess";
import { SendText } from "jslib-common/models/domain/sendText";
import { SendAccessResponse } from "jslib-common/models/response/sendAccessResponse";
import { mockEnc } from "../utils";
describe("SendAccess", () => {
let request: SendAccessResponse;
beforeEach(() => {
request = {
id: "id",
type: SendType.Text,
name: "encName",
file: null,
text: {
text: "encText",
hidden: true,
},
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
creatorIdentifier: "creatorIdentifier",
} as SendAccessResponse;
});
it("Convert from empty", () => {
const request = new SendAccessResponse({});
const sendAccess = new SendAccess(request);
expect(sendAccess).toEqual({
id: null,
type: undefined,
name: null,
creatorIdentifier: null,
expirationDate: null,
});
});
it("Convert", () => {
const sendAccess = new SendAccess(request);
expect(sendAccess).toEqual({
id: "id",
type: 0,
name: { encryptedString: "encName", encryptionType: 0 },
text: {
hidden: true,
text: { encryptedString: "encText", encryptionType: 0 },
},
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
creatorIdentifier: "creatorIdentifier",
});
});
it("Decrypt", async () => {
const sendAccess = new SendAccess();
sendAccess.id = "id";
sendAccess.type = SendType.Text;
sendAccess.name = mockEnc("name");
const text = Substitute.for<SendText>();
text.decrypt(Arg.any()).resolves({} as any);
sendAccess.text = text;
sendAccess.expirationDate = new Date("2022-01-31T12:00:00.000Z");
sendAccess.creatorIdentifier = "creatorIdentifier";
const view = await sendAccess.decrypt(null);
text.received(1).decrypt(Arg.any());
expect(view).toEqual({
id: "id",
type: 0,
name: "name",
text: {},
file: expect.anything(),
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
creatorIdentifier: "creatorIdentifier",
});
});
});

View File

@@ -0,0 +1,57 @@
import { SendFileData } from "jslib-common/models/data/sendFileData";
import { SendFile } from "jslib-common/models/domain/sendFile";
import { mockEnc } from "../utils";
describe("SendFile", () => {
let data: SendFileData;
beforeEach(() => {
data = {
id: "id",
size: "1100",
sizeName: "1.1 KB",
fileName: "encFileName",
};
});
it("Convert from empty", () => {
const data = new SendFileData();
const sendFile = new SendFile(data);
expect(sendFile).toEqual({
fileName: null,
id: null,
size: undefined,
sizeName: null,
});
});
it("Convert", () => {
const sendFile = new SendFile(data);
expect(sendFile).toEqual({
id: "id",
size: "1100",
sizeName: "1.1 KB",
fileName: { encryptedString: "encFileName", encryptionType: 0 },
});
});
it("Decrypt", async () => {
const sendFile = new SendFile();
sendFile.id = "id";
sendFile.size = "1100";
sendFile.sizeName = "1.1 KB";
sendFile.fileName = mockEnc("fileName");
const view = await sendFile.decrypt(null);
expect(view).toEqual({
fileName: "fileName",
id: "id",
size: "1100",
sizeName: "1.1 KB",
});
});
});

View File

@@ -0,0 +1,47 @@
import { SendTextData } from "jslib-common/models/data/sendTextData";
import { SendText } from "jslib-common/models/domain/sendText";
import { mockEnc } from "../utils";
describe("SendText", () => {
let data: SendTextData;
beforeEach(() => {
data = {
text: "encText",
hidden: false,
};
});
it("Convert from empty", () => {
const data = new SendTextData();
const secureNote = new SendText(data);
expect(secureNote).toEqual({
hidden: undefined,
text: null,
});
});
it("Convert", () => {
const secureNote = new SendText(data);
expect(secureNote).toEqual({
hidden: false,
text: { encryptedString: "encText", encryptionType: 0 },
});
});
it("Decrypt", async () => {
const secureNote = new SendText();
secureNote.text = mockEnc("text");
secureNote.hidden = true;
const view = await secureNote.decrypt(null);
expect(view).toEqual({
text: "text",
hidden: true,
});
});
});

View File

@@ -0,0 +1,69 @@
import { EncryptionType } from "jslib-common/enums/encryptionType";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { makeStaticByteArray } from "../utils";
describe("SymmetricCryptoKey", () => {
it("errors if no key", () => {
const t = () => {
new SymmetricCryptoKey(null);
};
expect(t).toThrowError("Must provide key");
});
describe("guesses encKey from key length", () => {
it("AesCbc256_B64", () => {
const key = makeStaticByteArray(32);
const cryptoKey = new SymmetricCryptoKey(key);
expect(cryptoKey).toEqual({
encKey: key,
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
encType: 0,
key: key,
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
macKey: null,
});
});
it("AesCbc128_HmacSha256_B64", () => {
const key = makeStaticByteArray(32);
const cryptoKey = new SymmetricCryptoKey(key, EncryptionType.AesCbc128_HmacSha256_B64);
expect(cryptoKey).toEqual({
encKey: key.slice(0, 16),
encKeyB64: "AAECAwQFBgcICQoLDA0ODw==",
encType: 1,
key: key,
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
macKey: key.slice(16, 32),
macKeyB64: "EBESExQVFhcYGRobHB0eHw==",
});
});
it("AesCbc256_HmacSha256_B64", () => {
const key = makeStaticByteArray(64);
const cryptoKey = new SymmetricCryptoKey(key);
expect(cryptoKey).toEqual({
encKey: key.slice(0, 32),
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
encType: 2,
key: key,
keyB64:
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==",
macKey: key.slice(32, 64),
macKeyB64: "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=",
});
});
it("unknown length", () => {
const t = () => {
new SymmetricCryptoKey(makeStaticByteArray(30));
};
expect(t).toThrowError("Unable to determine encType.");
});
});
});

View File

@@ -11,6 +11,9 @@ import { CreditCardData } from "./testData/onePassword1Pux/CreditCard";
import { DatabaseData } from "./testData/onePassword1Pux/Database";
import { DriversLicenseData } from "./testData/onePassword1Pux/DriversLicense";
import { EmailAccountData } from "./testData/onePassword1Pux/EmailAccount";
import { EmailFieldData } from "./testData/onePassword1Pux/Emailfield";
import { EmailFieldOnIdentityData } from "./testData/onePassword1Pux/EmailfieldOnIdentity";
import { EmailFieldOnIdentityPrefilledData } from "./testData/onePassword1Pux/EmailfieldOnIdentity_Prefilled";
import { IdentityData } from "./testData/onePassword1Pux/IdentityData";
import { LoginData } from "./testData/onePassword1Pux/LoginData";
import { MedicalRecordData } from "./testData/onePassword1Pux/MedicalRecord";
@@ -102,6 +105,25 @@ describe("1Password 1Pux Importer", () => {
expect(cipher.fields[1].type).toBe(FieldType.Boolean);
});
it("should add fields of type email as custom fields", async () => {
const importer = new Importer();
const EmailFieldDataJson = JSON.stringify(EmailFieldData);
const result = await importer.parse(EmailFieldDataJson);
expect(result != null).toBe(true);
const ciphers = result.ciphers;
expect(ciphers.length).toEqual(1);
const cipher = ciphers.shift();
expect(cipher.fields[0].name).toEqual("reg_email");
expect(cipher.fields[0].value).toEqual("kriddler@nullvalue.test");
expect(cipher.fields[0].type).toBe(FieldType.Text);
expect(cipher.fields[1].name).toEqual("provider");
expect(cipher.fields[1].value).toEqual("myEmailProvider");
expect(cipher.fields[1].type).toBe(FieldType.Text);
});
it('should create concealed field as "hidden" type', async () => {
const importer = new Importer();
const result = await importer.parse(OnePuxExampleFileJson);
@@ -205,6 +227,46 @@ describe("1Password 1Pux Importer", () => {
validateCustomField(cipher.fields, "forumsig", "super cool guy");
});
it("emails fields on identity types should be added to the identity email field", async () => {
const importer = new Importer();
const EmailFieldOnIdentityDataJson = JSON.stringify(EmailFieldOnIdentityData);
const result = await importer.parse(EmailFieldOnIdentityDataJson);
expect(result != null).toBe(true);
const ciphers = result.ciphers;
expect(ciphers.length).toEqual(1);
const cipher = ciphers.shift();
const identity = cipher.identity;
expect(identity.email).toEqual("gengels@nullvalue.test");
expect(cipher.fields[0].name).toEqual("provider");
expect(cipher.fields[0].value).toEqual("myEmailProvider");
expect(cipher.fields[0].type).toBe(FieldType.Text);
});
it("emails fields on identity types should be added to custom fields if identity.email has been filled", async () => {
const importer = new Importer();
const EmailFieldOnIdentityPrefilledDataJson = JSON.stringify(EmailFieldOnIdentityPrefilledData);
const result = await importer.parse(EmailFieldOnIdentityPrefilledDataJson);
expect(result != null).toBe(true);
const ciphers = result.ciphers;
expect(ciphers.length).toEqual(1);
const cipher = ciphers.shift();
const identity = cipher.identity;
expect(identity.email).toEqual("gengels@nullvalue.test");
expect(cipher.fields[0].name).toEqual("2nd_email");
expect(cipher.fields[0].value).toEqual("kriddler@nullvalue.test");
expect(cipher.fields[0].type).toBe(FieldType.Text);
expect(cipher.fields[1].name).toEqual("provider");
expect(cipher.fields[1].value).toEqual("myEmailProvider");
expect(cipher.fields[1].type).toBe(FieldType.Text);
});
it("should parse category 005 - Password (Legacy)", async () => {
const importer = new Importer();
const jsonString = JSON.stringify(PasswordData);

View File

@@ -0,0 +1,91 @@
import { ExportData } from "jslib-common/importers/onepasswordImporters/types/onepassword1PuxImporterTypes";
export const EmailFieldData: ExportData = {
accounts: [
{
attrs: {
accountName: "1Password Customer",
name: "1Password Customer",
avatar: "",
email: "username123123123@gmail.com",
uuid: "TRIZ3XV4JJFRXJ3BARILLTUA6E",
domain: "https://my.1password.com/",
},
vaults: [
{
attrs: {
uuid: "pqcgbqjxr4tng2hsqt5ffrgwju",
desc: "Just test entries",
avatar: "ke7i5rxnjrh3tj6uesstcosspu.png",
name: "T's Test Vault",
type: "U",
},
items: [
{
uuid: "47hvppiuwbanbza7bq6jpdjfxu",
favIndex: 1,
createdAt: 1619467985,
updatedAt: 1619468230,
trashed: false,
categoryUuid: "100",
details: {
loginFields: [],
notesPlain: "My Software License",
sections: [
{
title: "",
fields: [],
},
{
title: "Customer",
name: "customer",
fields: [
{
title: "registered email",
id: "reg_email",
value: {
email: {
email_address: "kriddler@nullvalue.test",
provider: "myEmailProvider",
},
},
indexAtSource: 1,
guarded: false,
multiline: false,
dontGenerate: false,
inputTraits: {
keyboard: "emailAddress",
correction: "default",
capitalization: "default",
},
},
],
},
{
title: "Publisher",
name: "publisher",
fields: [],
},
{
title: "Order",
name: "order",
fields: [],
},
],
passwordHistory: [],
},
overview: {
subtitle: "5.10.1000",
title: "Limux Product Key",
url: "",
ps: 0,
pbe: 0.0,
pgrng: false,
},
},
],
},
],
},
],
};

View File

@@ -0,0 +1,87 @@
import { ExportData } from "jslib-common/importers/onepasswordImporters/types/onepassword1PuxImporterTypes";
export const EmailFieldOnIdentityData: ExportData = {
accounts: [
{
attrs: {
accountName: "1Password Customer",
name: "1Password Customer",
avatar: "",
email: "username123123123@gmail.com",
uuid: "TRIZ3XV4JJFRXJ3BARILLTUA6E",
domain: "https://my.1password.com/",
},
vaults: [
{
attrs: {
uuid: "pqcgbqjxr4tng2hsqt5ffrgwju",
desc: "Just test entries",
avatar: "ke7i5rxnjrh3tj6uesstcosspu.png",
name: "T's Test Vault",
type: "U",
},
items: [
{
uuid: "45mjttbbq3owgij2uis55pfrlq",
favIndex: 0,
createdAt: 1619465450,
updatedAt: 1619465789,
trashed: false,
categoryUuid: "004",
details: {
loginFields: [],
notesPlain: "",
sections: [
{
title: "Identification",
name: "name",
fields: [],
},
{
title: "Address",
name: "address",
fields: [],
},
{
title: "Internet Details",
name: "internet",
fields: [
{
title: "E-mail",
id: "E-mail",
value: {
email: {
email_address: "gengels@nullvalue.test",
provider: "myEmailProvider",
},
},
indexAtSource: 4,
guarded: false,
multiline: false,
dontGenerate: false,
inputTraits: {
keyboard: "emailAddress",
correction: "default",
capitalization: "default",
},
},
],
},
],
passwordHistory: [],
},
overview: {
subtitle: "George Engels",
title: "George Engels",
url: "",
ps: 0,
pbe: 0.0,
pgrng: false,
},
},
],
},
],
},
],
};

View File

@@ -0,0 +1,103 @@
import { ExportData } from "jslib-common/importers/onepasswordImporters/types/onepassword1PuxImporterTypes";
export const EmailFieldOnIdentityPrefilledData: ExportData = {
accounts: [
{
attrs: {
accountName: "1Password Customer",
name: "1Password Customer",
avatar: "",
email: "username123123123@gmail.com",
uuid: "TRIZ3XV4JJFRXJ3BARILLTUA6E",
domain: "https://my.1password.com/",
},
vaults: [
{
attrs: {
uuid: "pqcgbqjxr4tng2hsqt5ffrgwju",
desc: "Just test entries",
avatar: "ke7i5rxnjrh3tj6uesstcosspu.png",
name: "T's Test Vault",
type: "U",
},
items: [
{
uuid: "45mjttbbq3owgij2uis55pfrlq",
favIndex: 0,
createdAt: 1619465450,
updatedAt: 1619465789,
trashed: false,
categoryUuid: "004",
details: {
loginFields: [],
notesPlain: "",
sections: [
{
title: "Identification",
name: "name",
fields: [],
},
{
title: "Address",
name: "address",
fields: [],
},
{
title: "Internet Details",
name: "internet",
fields: [
{
title: "email",
id: "email",
value: {
string: "gengels@nullvalue.test",
},
indexAtSource: 4,
guarded: false,
multiline: false,
dontGenerate: false,
inputTraits: {
keyboard: "emailAddress",
correction: "default",
capitalization: "default",
},
},
{
title: "2nd email",
id: "2nd_email",
value: {
email: {
email_address: "kriddler@nullvalue.test",
provider: "myEmailProvider",
},
},
indexAtSource: 1,
guarded: false,
multiline: false,
dontGenerate: false,
inputTraits: {
keyboard: "emailAddress",
correction: "default",
capitalization: "default",
},
},
],
},
],
passwordHistory: [],
},
overview: {
subtitle: "George Engels",
title: "George Engels",
url: "",
ps: 0,
pbe: 0.0,
pgrng: false,
},
},
],
},
],
},
],
};

View File

@@ -344,7 +344,10 @@ export const SanitizedExport: ExportData = {
title: "",
id: "irpvnshg5kjpkmj5jwy4xxkfom",
value: {
email: "plexuser@nullvalue.test",
email: {
email_address: "plexuser@nullvalue.test",
provider: null,
},
},
indexAtSource: 0,
guarded: false,
@@ -1434,7 +1437,10 @@ export const SanitizedExport: ExportData = {
title: "registered email",
id: "reg_email",
value: {
email: "kriddler@nullvalue.test",
email: {
email_address: "kriddler@nullvalue.test",
provider: null,
},
},
indexAtSource: 1,
guarded: false,
@@ -1536,7 +1542,10 @@ export const SanitizedExport: ExportData = {
title: "support email",
id: "support_email",
value: {
email: "support@nullvalue.test",
email: {
email_address: "support@nullvalue.test",
provider: null,
},
},
indexAtSource: 4,
guarded: false,
@@ -4014,7 +4023,10 @@ export const SanitizedExport: ExportData = {
title: "registered email",
id: "reg_email",
value: {
email: "",
email: {
email_address: "",
provider: null,
},
},
indexAtSource: 1,
guarded: false,
@@ -4116,7 +4128,10 @@ export const SanitizedExport: ExportData = {
title: "support email",
id: "support_email",
value: {
email: "",
email: {
email_address: "",
provider: null,
},
},
indexAtSource: 4,
guarded: false,

View File

@@ -93,7 +93,10 @@ export const SoftwareLicenseData: ExportData = {
title: "registered email",
id: "reg_email",
value: {
email: "kriddler@nullvalue.test",
email: {
email_address: "kriddler@nullvalue.test",
provider: null,
},
},
indexAtSource: 1,
guarded: false,
@@ -195,7 +198,10 @@ export const SoftwareLicenseData: ExportData = {
title: "support email",
id: "support_email",
value: {
email: "support@nullvalue.test",
email: {
email_address: "support@nullvalue.test",
provider: null,
},
},
indexAtSource: 4,
guarded: false,

View File

@@ -11,7 +11,7 @@ import { Utils } from "jslib-common/misc/utils";
import { Cipher } from "jslib-common/models/domain/cipher";
import { EncString } from "jslib-common/models/domain/encString";
import { Login } from "jslib-common/models/domain/login";
import { CipherWithIds as CipherExport } from "jslib-common/models/export/cipherWithIds";
import { CipherWithIdExport as CipherExport } from "jslib-common/models/export/cipherWithIdsExport";
import { CipherView } from "jslib-common/models/view/cipherView";
import { LoginView } from "jslib-common/models/view/loginView";
import { ExportService } from "jslib-common/services/export.service";

View File

@@ -1,3 +1,7 @@
import Substitute, { Arg } from "@fluffy-spoon/substitute";
import { EncString } from "jslib-common/models/domain/encString";
function newGuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
@@ -16,3 +20,18 @@ export function BuildTestObject<T, K extends keyof T = keyof T>(
): T {
return Object.assign(constructor === null ? {} : new constructor(), def) as T;
}
export function mockEnc(s: string): EncString {
const mock = Substitute.for<EncString>();
mock.decrypt(Arg.any(), Arg.any()).resolves(s);
return mock;
}
export function makeStaticByteArray(length: number) {
const arr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
arr[i] = i;
}
return arr;
}

View File

@@ -1,3 +1,12 @@
import { OrganizationConnectionType } from "jslib-common/enums/organizationConnectionType";
import { OrganizationConnectionRequest } from "jslib-common/models/request/organizationConnectionRequest";
import { BillingHistoryResponse } from "jslib-common/models/response/billingHistoryResponse";
import { BillingPaymentResponse } from "jslib-common/models/response/billingPaymentResponse";
import {
OrganizationConnectionConfigApis,
OrganizationConnectionResponse,
} from "jslib-common/models/response/organizationConnectionResponse";
import { PolicyType } from "../enums/policyType";
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
import { VerifyOTPRequest } from "../models/request/account/verifyOTPRequest";
@@ -36,6 +45,7 @@ import { KeysRequest } from "../models/request/keysRequest";
import { OrganizationSponsorshipCreateRequest } from "../models/request/organization/organizationSponsorshipCreateRequest";
import { OrganizationSponsorshipRedeemRequest } from "../models/request/organization/organizationSponsorshipRedeemRequest";
import { OrganizationSsoRequest } from "../models/request/organization/organizationSsoRequest";
import { OrganizationApiKeyRequest } from "../models/request/organizationApiKeyRequest";
import { OrganizationCreateRequest } from "../models/request/organizationCreateRequest";
import { OrganizationImportRequest } from "../models/request/organizationImportRequest";
import { OrganizationKeysRequest } from "../models/request/organizationKeysRequest";
@@ -118,9 +128,11 @@ import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorR
import { KeyConnectorUserKeyResponse } from "../models/response/keyConnectorUserKeyResponse";
import { ListResponse } from "../models/response/listResponse";
import { OrganizationSsoResponse } from "../models/response/organization/organizationSsoResponse";
import { OrganizationApiKeyInformationResponse } from "../models/response/organizationApiKeyInformationResponse";
import { OrganizationAutoEnrollStatusResponse } from "../models/response/organizationAutoEnrollStatusResponse";
import { OrganizationKeysResponse } from "../models/response/organizationKeysResponse";
import { OrganizationResponse } from "../models/response/organizationResponse";
import { OrganizationSponsorshipSyncStatusResponse } from "../models/response/organizationSponsorshipSyncStatusResponse";
import { OrganizationSubscriptionResponse } from "../models/response/organizationSubscriptionResponse";
import { OrganizationUserBulkPublicKeyResponse } from "../models/response/organizationUserBulkPublicKeyResponse";
import { OrganizationUserBulkResponse } from "../models/response/organizationUserBulkResponse";
@@ -174,7 +186,6 @@ export abstract class ApiService {
refreshIdentityToken: () => Promise<any>;
getProfile: () => Promise<ProfileResponse>;
getUserBilling: () => Promise<BillingResponse>;
getUserSubscription: () => Promise<SubscriptionResponse>;
getTaxInfo: () => Promise<TaxInfoResponse>;
putProfile: (request: UpdateProfileRequest) => Promise<ProfileResponse>;
@@ -212,6 +223,9 @@ export abstract class ApiService {
postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise<void>;
postConvertToKeyConnector: () => Promise<void>;
getUserBillingHistory: () => Promise<BillingHistoryResponse>;
getUserBillingPayment: () => Promise<BillingPaymentResponse>;
getFolder: (id: string) => Promise<FolderResponse>;
postFolder: (request: FolderRequest) => Promise<FolderResponse>;
putFolder: (id: string, request: FolderRequest) => Promise<FolderResponse>;
@@ -503,6 +517,22 @@ export abstract class ApiService {
getOrganization: (id: string) => Promise<OrganizationResponse>;
getOrganizationBilling: (id: string) => Promise<BillingResponse>;
getOrganizationSubscription: (id: string) => Promise<OrganizationSubscriptionResponse>;
getCloudCommunicationsEnabled: () => Promise<boolean>;
abstract getOrganizationConnection<TConfig extends OrganizationConnectionConfigApis>(
id: string,
type: OrganizationConnectionType,
configType: { new (response: any): TConfig }
): Promise<OrganizationConnectionResponse<TConfig>>;
abstract createOrganizationConnection<TConfig extends OrganizationConnectionConfigApis>(
request: OrganizationConnectionRequest,
configType: { new (response: any): TConfig }
): Promise<OrganizationConnectionResponse<TConfig>>;
abstract updateOrganizationConnection<TConfig extends OrganizationConnectionConfigApis>(
request: OrganizationConnectionRequest,
configType: { new (response: any): TConfig },
organizationConnectionId: string
): Promise<OrganizationConnectionResponse<TConfig>>;
deleteOrganizationConnection: (id: string) => Promise<void>;
getOrganizationLicense: (id: string, installationId: string) => Promise<any>;
getOrganizationTaxInfo: (id: string) => Promise<TaxInfoResponse>;
getOrganizationAutoEnrollStatus: (
@@ -520,11 +550,14 @@ export abstract class ApiService {
postOrganizationLicenseUpdate: (id: string, data: FormData) => Promise<any>;
postOrganizationApiKey: (
id: string,
request: SecretVerificationRequest
request: OrganizationApiKeyRequest
) => Promise<ApiKeyResponse>;
getOrganizationApiKeyInformation: (
id: string
) => Promise<ListResponse<OrganizationApiKeyInformationResponse>>;
postOrganizationRotateApiKey: (
id: string,
request: SecretVerificationRequest
request: OrganizationApiKeyRequest
) => Promise<ApiKeyResponse>;
postOrganizationSso: (
id: string,
@@ -661,6 +694,9 @@ export abstract class ApiService {
sponsorshipOrgId: string,
request: OrganizationSponsorshipCreateRequest
) => Promise<void>;
getSponsorshipSyncStatus: (
sponsoredOrgId: string
) => Promise<OrganizationSponsorshipSyncStatusResponse>;
deleteRevokeSponsorship: (sponsoringOrganizationId: string) => Promise<void>;
deleteRemoveSponsorship: (sponsoringOrgId: string) => Promise<void>;
postPreValidateSponsorshipToken: (sponsorshipToken: string) => Promise<boolean>;

View File

@@ -1,3 +1,4 @@
import { AuthenticationStatus } from "../enums/authenticationStatus";
import { AuthResult } from "../models/domain/authResult";
import {
ApiLogInCredentials,
@@ -22,4 +23,5 @@ export abstract class AuthService {
authingWithApiKey: () => boolean;
authingWithSso: () => boolean;
authingWithPassword: () => boolean;
getAuthStatus: (userId?: string) => Promise<AuthenticationStatus>;
}

View File

@@ -1,6 +0,0 @@
export abstract class BiometricMain {
isError: boolean;
init: () => Promise<void>;
supportsBiometric: () => Promise<boolean>;
authenticateBiometric: () => Promise<boolean>;
}

View File

@@ -10,7 +10,7 @@ export abstract class FolderService {
get: (id: string) => Promise<Folder>;
getAll: () => Promise<Folder[]>;
getAllDecrypted: () => Promise<FolderView[]>;
getAllNested: () => Promise<TreeNode<FolderView>[]>;
getAllNested: (folders?: FolderView[]) => Promise<TreeNode<FolderView>[]>;
getNested: (id: string) => Promise<TreeNode<FolderView>>;
saveWithServer: (folder: Folder) => Promise<any>;
upsert: (folder: FolderData | FolderData[]) => Promise<any>;

View File

@@ -242,8 +242,6 @@ export abstract class StateService<T extends Account = Account> {
setLocalData: (value: string, options?: StorageOptions) => Promise<void>;
getLocale: (options?: StorageOptions) => Promise<string>;
setLocale: (value: string, options?: StorageOptions) => Promise<void>;
getLoginRedirect: (options?: StorageOptions) => Promise<any>;
setLoginRedirect: (value: any, options?: StorageOptions) => Promise<void>;
getMainWindowSize: (options?: StorageOptions) => Promise<number>;
setMainWindowSize: (value: number, options?: StorageOptions) => Promise<void>;
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;

View File

@@ -3,6 +3,7 @@ export abstract class UsernameGenerationService {
generateWord: (options: any) => Promise<string>;
generateSubaddress: (options: any) => Promise<string>;
generateCatchall: (options: any) => Promise<string>;
generateForwarded: (options: any) => Promise<string>;
getOptions: () => Promise<any>;
saveOptions: (options: any) => Promise<void>;
}

View File

@@ -1,5 +1,4 @@
export abstract class VaultTimeoutService {
isLocked: (userId?: string) => Promise<boolean>;
checkVaultTimeout: () => Promise<void>;
lock: (allowSoftLock?: boolean, userId?: string) => Promise<void>;
logOut: (userId?: string) => Promise<void>;

View File

@@ -1,6 +1,5 @@
export enum AuthenticationStatus {
Locked = "locked",
Unlocked = "unlocked",
LoggedOut = "loggedOut",
Active = "active",
LoggedOut = 0,
Locked = 1,
Unlocked = 2,
}

View File

@@ -0,0 +1,4 @@
export enum OrganizationApiKeyType {
Default = 0,
BillingSync = 1,
}

View File

@@ -0,0 +1,3 @@
export enum OrganizationConnectionType {
CloudBillingSync = 1,
}

View File

@@ -24,4 +24,5 @@ export enum Permissions {
EditAssignedCollections,
DeleteAssignedCollections,
ManageSso,
ManageBilling,
}

View File

@@ -2,9 +2,9 @@ import { CryptoService } from "../abstractions/crypto.service";
import { I18nService } from "../abstractions/i18n.service";
import { EncString } from "../models/domain/encString";
import { ImportResult } from "../models/domain/importResult";
import { CipherWithIds } from "../models/export/cipherWithIds";
import { CollectionWithId } from "../models/export/collectionWithId";
import { FolderWithId } from "../models/export/folderWithId";
import { CipherWithIdExport } from "../models/export/cipherWithIdsExport";
import { CollectionWithIdExport } from "../models/export/collectionWithIdExport";
import { FolderWithIdExport } from "../models/export/folderWithIdExport";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
@@ -59,8 +59,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
const groupingsMap = new Map<string, number>();
if (this.organization && this.results.collections != null) {
for (const c of this.results.collections as CollectionWithId[]) {
const collection = CollectionWithId.toDomain(c);
for (const c of this.results.collections as CollectionWithIdExport[]) {
const collection = CollectionWithIdExport.toDomain(c);
if (collection != null) {
collection.id = null;
collection.organizationId = this.organizationId;
@@ -70,8 +70,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
}
}
} else if (!this.organization && this.results.folders != null) {
for (const f of this.results.folders as FolderWithId[]) {
const folder = FolderWithId.toDomain(f);
for (const f of this.results.folders as FolderWithIdExport[]) {
const folder = FolderWithIdExport.toDomain(f);
if (folder != null) {
folder.id = null;
const view = await folder.decrypt();
@@ -81,8 +81,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
}
}
for (const c of this.results.items as CipherWithIds[]) {
const cipher = CipherWithIds.toDomain(c);
for (const c of this.results.items as CipherWithIdExport[]) {
const cipher = CipherWithIdExport.toDomain(c);
// reset ids incase they were set for some reason
cipher.id = null;
cipher.folderId = null;
@@ -121,8 +121,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
private parseDecrypted() {
const groupingsMap = new Map<string, number>();
if (this.organization && this.results.collections != null) {
this.results.collections.forEach((c: CollectionWithId) => {
const collection = CollectionWithId.toView(c);
this.results.collections.forEach((c: CollectionWithIdExport) => {
const collection = CollectionWithIdExport.toView(c);
if (collection != null) {
collection.id = null;
collection.organizationId = null;
@@ -131,8 +131,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
}
});
} else if (!this.organization && this.results.folders != null) {
this.results.folders.forEach((f: FolderWithId) => {
const folder = FolderWithId.toView(f);
this.results.folders.forEach((f: FolderWithIdExport) => {
const folder = FolderWithIdExport.toView(f);
if (folder != null) {
folder.id = null;
groupingsMap.set(f.id, this.result.folders.length);
@@ -141,8 +141,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
});
}
this.results.items.forEach((c: CipherWithIds) => {
const cipher = CipherWithIds.toView(c);
this.results.items.forEach((c: CipherWithIdExport) => {
const cipher = CipherWithIdExport.toView(c);
// reset ids incase they were set for some reason
cipher.id = null;
cipher.folderId = null;

View File

@@ -258,7 +258,7 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer {
}
}
} else if (cipher.type === CipherType.Identity) {
if (this.fillIdentity(field, fieldValue, cipher)) {
if (this.fillIdentity(field, fieldValue, cipher, valueKey)) {
return;
}
if (valueKey === "address") {
@@ -312,6 +312,14 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer {
}
}
if (valueKey === "email") {
// fieldValue is an object casted into a string, so access the plain value instead
const { email_address, provider } = field.value.email;
this.processKvp(cipher, fieldName, email_address, FieldType.Text);
this.processKvp(cipher, "provider", provider, FieldType.Text);
return;
}
// Do not include a password field if it's already in the history
if (
field.title === "password" &&
@@ -440,7 +448,12 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer {
return false;
}
private fillIdentity(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean {
private fillIdentity(
field: FieldsEntity,
fieldValue: string,
cipher: CipherView,
valueKey: string
): boolean {
if (this.isNullOrWhitespace(cipher.identity.firstName) && field.id === "firstname") {
cipher.identity.firstName = fieldValue;
return true;
@@ -466,9 +479,18 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer {
return true;
}
if (this.isNullOrWhitespace(cipher.identity.email) && field.id === "email") {
cipher.identity.email = fieldValue;
return true;
if (this.isNullOrWhitespace(cipher.identity.email)) {
if (valueKey === "email") {
const { email_address, provider } = field.value.email;
cipher.identity.email = this.getValueOrDefault(email_address);
this.processKvp(cipher, "provider", provider, FieldType.Text);
return true;
}
if (field.id === "email") {
cipher.identity.email = fieldValue;
return true;
}
}
if (this.isNullOrWhitespace(cipher.identity.username) && field.id === "username") {

View File

@@ -106,7 +106,7 @@ export interface Value {
date?: number | null;
string?: string | null;
concealed?: string | null;
email?: string | null;
email?: Email | null;
phone?: string | null;
menu?: string | null;
gender?: string | null;
@@ -117,6 +117,12 @@ export interface Value {
creditCardNumber?: string | null;
reference?: string | null;
}
export interface Email {
email_address: string;
provider: string;
}
export interface Address {
street: string;
city: string;

View File

@@ -0,0 +1,13 @@
import { BaseResponse } from "../response/baseResponse";
export class BillingSyncConfigApi extends BaseResponse {
billingSyncKey: string;
constructor(data: any) {
super(data);
if (data == null) {
return;
}
this.billingSyncKey = this.getResponseProperty("BillingSyncKey");
}
}

View File

@@ -3,7 +3,6 @@ import { BaseResponse } from "../response/baseResponse";
export class SendFileApi extends BaseResponse {
id: string;
fileName: string;
key: string;
size: string;
sizeName: string;
@@ -14,7 +13,6 @@ export class SendFileApi extends BaseResponse {
}
this.id = this.getResponseProperty("Id");
this.fileName = this.getResponseProperty("FileName");
this.key = this.getResponseProperty("Key");
this.size = this.getResponseProperty("Size");
this.sizeName = this.getResponseProperty("SizeName");
}

View File

@@ -14,14 +14,12 @@ export class CipherData {
id: string;
organizationId: string;
folderId: string;
userId: string;
edit: boolean;
viewPassword: boolean;
organizationUseTotp: boolean;
favorite: boolean;
revisionDate: string;
type: CipherType;
sizeName: string;
name: string;
notes: string;
login?: LoginData;
@@ -35,7 +33,7 @@ export class CipherData {
deletedDate: string;
reprompt: CipherRepromptType;
constructor(response?: CipherResponse, userId?: string, collectionIds?: string[]) {
constructor(response?: CipherResponse, collectionIds?: string[]) {
if (response == null) {
return;
}
@@ -43,7 +41,6 @@ export class CipherData {
this.id = response.id;
this.organizationId = response.organizationId;
this.folderId = response.folderId;
this.userId = userId;
this.edit = response.edit;
this.viewPassword = response.viewPassword;
this.organizationUseTotp = response.organizationUseTotp;

View File

@@ -2,12 +2,10 @@ import { FolderResponse } from "../response/folderResponse";
export class FolderData {
id: string;
userId: string;
name: string;
revisionDate: string;
constructor(response: FolderResponse, userId: string) {
this.userId = userId;
constructor(response: FolderResponse) {
this.name = response.name;
this.id = response.id;
this.revisionDate = response.revisionDate;

View File

@@ -39,6 +39,9 @@ export class OrganizationData {
planProductType: ProductType;
keyConnectorEnabled: boolean;
keyConnectorUrl: string;
familySponsorshipLastSyncDate?: Date;
familySponsorshipValidUntil?: Date;
familySponsorshipToDelete?: boolean;
constructor(response: ProfileOrganizationResponse) {
this.id = response.id;
@@ -74,5 +77,8 @@ export class OrganizationData {
this.planProductType = response.planProductType;
this.keyConnectorEnabled = response.keyConnectorEnabled;
this.keyConnectorUrl = response.keyConnectorUrl;
this.familySponsorshipLastSyncDate = response.familySponsorshipLastSyncDate;
this.familySponsorshipValidUntil = response.familySponsorshipValidUntil;
this.familySponsorshipToDelete = response.familySponsorshipToDelete;
}
}

View File

@@ -7,7 +7,6 @@ import { SendTextData } from "./sendTextData";
export class SendData {
id: string;
accessId: string;
userId: string;
type: SendType;
name: string;
notes: string;
@@ -23,14 +22,13 @@ export class SendData {
disabled: boolean;
hideEmail: boolean;
constructor(response?: SendResponse, userId?: string) {
constructor(response?: SendResponse) {
if (response == null) {
return;
}
this.id = response.id;
this.accessId = response.accessId;
this.userId = userId;
this.type = response.type;
this.name = response.name;
this.notes = response.notes;

View File

@@ -3,7 +3,6 @@ import { SendFileApi } from "../api/sendFileApi";
export class SendFileData {
id: string;
fileName: string;
key: string;
size: string;
sizeName: string;
@@ -14,7 +13,6 @@ export class SendFileData {
this.id = data.id;
this.fileName = data.fileName;
this.key = data.key;
this.size = data.size;
this.sizeName = data.sizeName;
}

View File

@@ -11,11 +11,11 @@ export class Attachment extends Domain {
id: string;
url: string;
size: string;
sizeName: string;
sizeName: string; // Readable size, ex: "4.2 KB" or "1.43 GB"
key: EncString;
fileName: EncString;
constructor(obj?: AttachmentData, alreadyEncrypted = false) {
constructor(obj?: AttachmentData) {
super();
if (obj == null) {
return;
@@ -32,7 +32,6 @@ export class Attachment extends Domain {
fileName: null,
key: null,
},
alreadyEncrypted,
["id", "url", "sizeName"]
);
}

View File

@@ -13,7 +13,7 @@ export class Card extends Domain {
expYear: EncString;
code: EncString;
constructor(obj?: CardData, alreadyEncrypted = false) {
constructor(obj?: CardData) {
super();
if (obj == null) {
return;
@@ -30,7 +30,6 @@ export class Card extends Domain {
expYear: null,
code: null,
},
alreadyEncrypted,
[]
);
}

View File

@@ -38,7 +38,7 @@ export class Cipher extends Domain {
deletedDate: Date;
reprompt: CipherRepromptType;
constructor(obj?: CipherData, alreadyEncrypted = false, localData: any = null) {
constructor(obj?: CipherData, localData: any = null) {
super();
if (obj == null) {
return;
@@ -49,14 +49,12 @@ export class Cipher extends Domain {
obj,
{
id: null,
userId: null,
organizationId: null,
folderId: null,
name: null,
notes: null,
},
alreadyEncrypted,
["id", "userId", "organizationId", "folderId"]
["id", "organizationId", "folderId"]
);
this.type = obj.type;
@@ -76,35 +74,35 @@ export class Cipher extends Domain {
switch (this.type) {
case CipherType.Login:
this.login = new Login(obj.login, alreadyEncrypted);
this.login = new Login(obj.login);
break;
case CipherType.SecureNote:
this.secureNote = new SecureNote(obj.secureNote);
break;
case CipherType.Card:
this.card = new Card(obj.card, alreadyEncrypted);
this.card = new Card(obj.card);
break;
case CipherType.Identity:
this.identity = new Identity(obj.identity, alreadyEncrypted);
this.identity = new Identity(obj.identity);
break;
default:
break;
}
if (obj.attachments != null) {
this.attachments = obj.attachments.map((a) => new Attachment(a, alreadyEncrypted));
this.attachments = obj.attachments.map((a) => new Attachment(a));
} else {
this.attachments = null;
}
if (obj.fields != null) {
this.fields = obj.fields.map((f) => new Field(f, alreadyEncrypted));
this.fields = obj.fields.map((f) => new Field(f));
} else {
this.fields = null;
}
if (obj.passwordHistory != null) {
this.passwordHistory = obj.passwordHistory.map((ph) => new Password(ph, alreadyEncrypted));
this.passwordHistory = obj.passwordHistory.map((ph) => new Password(ph));
} else {
this.passwordHistory = null;
}
@@ -187,12 +185,11 @@ export class Cipher extends Domain {
return model;
}
toCipherData(userId: string): CipherData {
toCipherData(): CipherData {
const c = new CipherData();
c.id = this.id;
c.organizationId = this.organizationId;
c.folderId = this.folderId;
c.userId = this.organizationId != null ? userId : null;
c.edit = this.edit;
c.viewPassword = this.viewPassword;
c.organizationUseTotp = this.organizationUseTotp;

View File

@@ -12,7 +12,7 @@ export class Collection extends Domain {
readOnly: boolean;
hidePasswords: boolean;
constructor(obj?: CollectionData, alreadyEncrypted = false) {
constructor(obj?: CollectionData) {
super();
if (obj == null) {
return;
@@ -29,7 +29,6 @@ export class Collection extends Domain {
readOnly: null,
hidePasswords: null,
},
alreadyEncrypted,
["id", "organizationId", "externalId", "readOnly", "hidePasswords"]
);
}

View File

@@ -8,7 +8,6 @@ export default class Domain {
domain: D,
dataObj: any,
map: any,
alreadyEncrypted: boolean,
notEncList: any[] = []
) {
for (const prop in map) {
@@ -18,7 +17,7 @@ export default class Domain {
}
const objProp = dataObj[map[prop] || prop];
if (alreadyEncrypted === true || notEncList.indexOf(prop) > -1) {
if (notEncList.indexOf(prop) > -1) {
(domain as any)[prop] = objProp ? objProp : null;
} else {
(domain as any)[prop] = objProp ? new EncString(objProp) : null;

View File

@@ -13,7 +13,7 @@ export class Field extends Domain {
type: FieldType;
linkedId: LinkedIdType;
constructor(obj?: FieldData, alreadyEncrypted = false) {
constructor(obj?: FieldData) {
super();
if (obj == null) {
return;
@@ -28,7 +28,6 @@ export class Field extends Domain {
name: null,
value: null,
},
alreadyEncrypted,
[]
);
}

View File

@@ -9,7 +9,7 @@ export class Folder extends Domain {
name: EncString;
revisionDate: Date;
constructor(obj?: FolderData, alreadyEncrypted = false) {
constructor(obj?: FolderData) {
super();
if (obj == null) {
return;
@@ -22,7 +22,6 @@ export class Folder extends Domain {
id: null,
name: null,
},
alreadyEncrypted,
["id"]
);

View File

@@ -7,7 +7,7 @@ import { WindowState } from "./windowState";
export class GlobalState {
enableAlwaysOnTop?: boolean;
installedVersion?: string;
locale?: string = "en";
locale?: string;
organizationInvitation?: any;
ssoCodeVerifier?: string;
ssoOrganizationIdentifier?: string;

View File

@@ -25,7 +25,7 @@ export class Identity extends Domain {
passportNumber: EncString;
licenseNumber: EncString;
constructor(obj?: IdentityData, alreadyEncrypted = false) {
constructor(obj?: IdentityData) {
super();
if (obj == null) {
return;
@@ -54,7 +54,6 @@ export class Identity extends Domain {
passportNumber: null,
licenseNumber: null,
},
alreadyEncrypted,
[]
);
}

View File

@@ -14,7 +14,7 @@ export class Login extends Domain {
totp: EncString;
autofillOnPageLoad: boolean;
constructor(obj?: LoginData, alreadyEncrypted = false) {
constructor(obj?: LoginData) {
super();
if (obj == null) {
return;
@@ -31,14 +31,13 @@ export class Login extends Domain {
password: null,
totp: null,
},
alreadyEncrypted,
[]
);
if (obj.uris) {
this.uris = [];
obj.uris.forEach((u) => {
this.uris.push(new LoginUri(u, alreadyEncrypted));
this.uris.push(new LoginUri(u));
});
}
}

View File

@@ -10,7 +10,7 @@ export class LoginUri extends Domain {
uri: EncString;
match: UriMatchType;
constructor(obj?: LoginUriData, alreadyEncrypted = false) {
constructor(obj?: LoginUriData) {
super();
if (obj == null) {
return;
@@ -23,7 +23,6 @@ export class LoginUri extends Domain {
{
uri: null,
},
alreadyEncrypted,
[]
);
}
@@ -46,6 +45,7 @@ export class LoginUri extends Domain {
u,
{
uri: null,
match: null,
},
["match"]
);

View File

@@ -1,5 +1,6 @@
import { OrganizationUserStatusType } from "../../enums/organizationUserStatusType";
import { OrganizationUserType } from "../../enums/organizationUserType";
import { Permissions } from "../../enums/permissions";
import { ProductType } from "../../enums/productType";
import { PermissionsApi } from "../api/permissionsApi";
import { OrganizationData } from "../data/organizationData";
@@ -39,6 +40,9 @@ export class Organization {
planProductType: ProductType;
keyConnectorEnabled: boolean;
keyConnectorUrl: string;
familySponsorshipLastSyncDate?: Date;
familySponsorshipValidUntil?: Date;
familySponsorshipToDelete?: boolean;
constructor(obj?: OrganizationData) {
if (obj == null) {
@@ -79,6 +83,9 @@ export class Organization {
this.planProductType = obj.planProductType;
this.keyConnectorEnabled = obj.keyConnectorEnabled;
this.keyConnectorUrl = obj.keyConnectorUrl;
this.familySponsorshipLastSyncDate = obj.familySponsorshipLastSyncDate;
this.familySponsorshipValidUntil = obj.familySponsorshipValidUntil;
this.familySponsorshipToDelete = obj.familySponsorshipToDelete;
}
get canAccess() {
@@ -181,4 +188,35 @@ export class Organization {
get isExemptFromPolicies() {
return this.canManagePolicies;
}
hasAnyPermission(permissions: Permissions[]) {
const specifiedPermissions =
(permissions.includes(Permissions.AccessEventLogs) && this.canAccessEventLogs) ||
(permissions.includes(Permissions.AccessImportExport) && this.canAccessImportExport) ||
(permissions.includes(Permissions.AccessReports) && this.canAccessReports) ||
(permissions.includes(Permissions.CreateNewCollections) && this.canCreateNewCollections) ||
(permissions.includes(Permissions.EditAnyCollection) && this.canEditAnyCollection) ||
(permissions.includes(Permissions.DeleteAnyCollection) && this.canDeleteAnyCollection) ||
(permissions.includes(Permissions.EditAssignedCollections) &&
this.canEditAssignedCollections) ||
(permissions.includes(Permissions.DeleteAssignedCollections) &&
this.canDeleteAssignedCollections) ||
(permissions.includes(Permissions.ManageGroups) && this.canManageGroups) ||
(permissions.includes(Permissions.ManageOrganization) && this.isOwner) ||
(permissions.includes(Permissions.ManagePolicies) && this.canManagePolicies) ||
(permissions.includes(Permissions.ManageUsers) && this.canManageUsers) ||
(permissions.includes(Permissions.ManageUsersPassword) && this.canManageUsersPassword) ||
(permissions.includes(Permissions.ManageSso) && this.canManageSso) ||
(permissions.includes(Permissions.ManageBilling) && this.canManageBilling);
return specifiedPermissions && (this.enabled || this.isOwner);
}
get canManageBilling() {
return this.isOwner && (this.isProviderUser || !this.hasProvider);
}
get hasProvider() {
return this.providerId != null || this.providerName != null;
}
}

View File

@@ -9,20 +9,15 @@ export class Password extends Domain {
password: EncString;
lastUsedDate: Date;
constructor(obj?: PasswordHistoryData, alreadyEncrypted = false) {
constructor(obj?: PasswordHistoryData) {
super();
if (obj == null) {
return;
}
this.buildDomainModel(
this,
obj,
{
password: null,
},
alreadyEncrypted
);
this.buildDomainModel(this, obj, {
password: null,
});
this.lastUsedDate = new Date(obj.lastUsedDate);
}

View File

@@ -12,7 +12,6 @@ import { SendText } from "./sendText";
export class Send extends Domain {
id: string;
accessId: string;
userId: string;
type: SendType;
name: EncString;
notes: EncString;
@@ -28,7 +27,7 @@ export class Send extends Domain {
disabled: boolean;
hideEmail: boolean;
constructor(obj?: SendData, alreadyEncrypted = false) {
constructor(obj?: SendData) {
super();
if (obj == null) {
return;
@@ -40,13 +39,11 @@ export class Send extends Domain {
{
id: null,
accessId: null,
userId: null,
name: null,
notes: null,
key: null,
},
alreadyEncrypted,
["id", "accessId", "userId"]
["id", "accessId"]
);
this.type = obj.type;
@@ -61,10 +58,10 @@ export class Send extends Domain {
switch (this.type) {
case SendType.Text:
this.text = new SendText(obj.text, alreadyEncrypted);
this.text = new SendText(obj.text);
break;
case SendType.File:
this.file = new SendFile(obj.file, alreadyEncrypted);
this.file = new SendFile(obj.file);
break;
default:
break;

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