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

Merge branch 'master' of https://github.com/bitwarden/jslib into feature/additional-item-types-scaffold

This commit is contained in:
Hinton
2022-04-29 10:38:28 +02:00
120 changed files with 22066 additions and 7968 deletions

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) [![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 # Bitwarden JavaScript Library
Common code referenced across Bitwarden JavaScript projects. Common code referenced across Bitwarden JavaScript projects.

View File

@@ -6,7 +6,6 @@ module.exports = {
name: "angular", name: "angular",
displayName: "angular tests", displayName: "angular tests",
preset: "jest-preset-angular", preset: "jest-preset-angular",
roots: ["<rootDir>/spec/"],
testMatch: ["**/+(*.)+(spec).+(ts)"], testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"], setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
collectCoverage: true, collectCoverage: true,

View File

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

View File

@@ -6,6 +6,10 @@ import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.se
import { ModalRef } from "./modal/modal.ref"; 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() @Directive()
export class PasswordRepromptComponent { export class PasswordRepromptComponent {
showPassword = false; showPassword = false;

View File

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

View File

@@ -61,7 +61,6 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
async cancel() { async cancel() {
await this.stateService.setOrganizationInvitation(null); await this.stateService.setOrganizationInvitation(null);
await this.stateService.setLoginRedirect(null);
this.router.navigate(["/vault"]); 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 { VerificationType } from "jslib-common/enums/verificationType";
import { Verification } from "jslib-common/types/verification"; 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({ @Component({
selector: "app-verify-master-password", selector: "app-user-verification",
templateUrl: "verify-master-password.component.html", templateUrl: "user-verification.component.html",
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
multi: true, multi: true,
useExisting: VerifyMasterPasswordComponent, useExisting: UserVerificationComponent,
}, },
], ],
animations: [ 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; usesKeyConnector = false;
disableRequestOTP = false; disableRequestOTP = false;
sentCode = false; sentCode = false;
@@ -41,7 +47,7 @@ export class VerifyMasterPasswordComponent implements ControlValueAccessor, OnIn
this.usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector(); this.usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
this.processChanges(this.secret.value); this.processChanges(this.secret.value);
this.secret.valueChanges.subscribe((secret) => this.processChanges(secret)); this.secret.valueChanges.subscribe((secret: string) => this.processChanges(secret));
} }
async requestOTP() { async requestOTP() {

View File

@@ -5,6 +5,12 @@ import { ErrorResponse } from "jslib-common/models/response/errorResponse";
import { ValidationService } from "../services/validation.service"; 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({ @Directive({
selector: "[appApiAction]", selector: "[appApiAction]",
}) })

View File

@@ -19,7 +19,7 @@ export class AuthGuardService implements CanActivate {
async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) {
const isAuthed = await this.stateService.getIsAuthenticated(); const isAuthed = await this.stateService.getIsAuthenticated();
if (!isAuthed) { if (!isAuthed) {
this.messagingService.send("authBlocked"); this.messagingService.send("authBlocked", { url: routerState.url });
return false; return false;
} }
@@ -28,16 +28,14 @@ export class AuthGuardService implements CanActivate {
if (routerState != null) { if (routerState != null) {
this.messagingService.send("lockedUrl", { url: routerState.url }); this.messagingService.send("lockedUrl", { url: routerState.url });
} }
this.router.navigate(["lock"], { queryParams: { promptBiometric: true } }); return this.router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } });
return false;
} }
if ( if (
!routerState.url.includes("remove-password") && !routerState.url.includes("remove-password") &&
(await this.keyConnectorService.getConvertAccountRequired()) (await this.keyConnectorService.getConvertAccountRequired())
) { ) {
this.router.navigate(["/remove-password"]); return this.router.createUrlTree(["/remove-password"]);
return false;
} }
return true; return true;

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 { ApiService as ApiServiceAbstraction } from "jslib-common/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "jslib-common/abstractions/appId.service"; import { AppIdService as AppIdServiceAbstraction } from "jslib-common/abstractions/appId.service";
@@ -82,20 +82,60 @@ import { PasswordRepromptService } from "./passwordReprompt.service";
import { UnauthGuardService } from "./unauth-guard.service"; import { UnauthGuardService } from "./unauth-guard.service";
import { ValidationService } from "./validation.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({ @NgModule({
declarations: [], declarations: [],
providers: [ providers: [
{ provide: "WINDOW", useValue: window },
{
provide: LOCALE_ID,
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
deps: [I18nServiceAbstraction],
},
ValidationService, ValidationService,
AuthGuardService, AuthGuardService,
UnauthGuardService, UnauthGuardService,
LockGuardService, LockGuardService,
ModalService, ModalService,
{ provide: WINDOW, useValue: window },
{
provide: LOCALE_ID,
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
deps: [I18nServiceAbstraction],
},
{
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, provide: AppIdServiceAbstraction,
useClass: AppIdService, useClass: AppIdService,
@@ -203,30 +243,17 @@ import { ValidationService } from "./validation.service";
{ {
provide: UsernameGenerationServiceAbstraction, provide: UsernameGenerationServiceAbstraction,
useClass: UsernameGenerationService, useClass: UsernameGenerationService,
deps: [CryptoServiceAbstraction, StateServiceAbstraction], deps: [CryptoServiceAbstraction, StateServiceAbstraction, ApiServiceAbstraction],
}, },
{ {
provide: ApiServiceAbstraction, provide: ApiServiceAbstraction,
useFactory: ( useClass: ApiService,
tokenService: TokenServiceAbstraction,
platformUtilsService: PlatformUtilsServiceAbstraction,
environmentService: EnvironmentServiceAbstraction,
messagingService: MessagingServiceAbstraction,
appIdService: AppIdServiceAbstraction
) =>
new ApiService(
tokenService,
platformUtilsService,
environmentService,
appIdService,
async (expired: boolean) => messagingService.send("logout", { expired: expired })
),
deps: [ deps: [
TokenServiceAbstraction, TokenServiceAbstraction,
PlatformUtilsServiceAbstraction, PlatformUtilsServiceAbstraction,
EnvironmentServiceAbstraction, EnvironmentServiceAbstraction,
MessagingServiceAbstraction,
AppIdServiceAbstraction, AppIdServiceAbstraction,
LOGOUT_CALLBACK,
], ],
}, },
{ {
@@ -236,39 +263,7 @@ import { ValidationService } from "./validation.service";
}, },
{ {
provide: SyncServiceAbstraction, provide: SyncServiceAbstraction,
useFactory: ( useClass: SyncService,
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 })
),
deps: [ deps: [
ApiServiceAbstraction, ApiServiceAbstraction,
SettingsServiceAbstraction, SettingsServiceAbstraction,
@@ -284,6 +279,7 @@ import { ValidationService } from "./validation.service";
StateServiceAbstraction, StateServiceAbstraction,
OrganizationServiceAbstraction, OrganizationServiceAbstraction,
ProviderServiceAbstraction, ProviderServiceAbstraction,
LOGOUT_CALLBACK,
], ],
}, },
{ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService }, { provide: BroadcasterServiceAbstraction, useClass: BroadcasterService },
@@ -294,35 +290,7 @@ import { ValidationService } from "./validation.service";
}, },
{ {
provide: VaultTimeoutServiceAbstraction, provide: VaultTimeoutServiceAbstraction,
useFactory: ( useClass: VaultTimeoutService,
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 })
),
deps: [ deps: [
CipherServiceAbstraction, CipherServiceAbstraction,
FolderServiceAbstraction, FolderServiceAbstraction,
@@ -335,42 +303,26 @@ import { ValidationService } from "./validation.service";
PolicyServiceAbstraction, PolicyServiceAbstraction,
KeyConnectorServiceAbstraction, KeyConnectorServiceAbstraction,
StateServiceAbstraction, StateServiceAbstraction,
LOCKED_CALLBACK,
LOGOUT_CALLBACK,
], ],
}, },
{ {
provide: StateServiceAbstraction, provide: StateServiceAbstraction,
useFactory: ( useClass: StateService,
storageService: StorageServiceAbstraction,
secureStorageService: StorageServiceAbstraction,
logService: LogService,
stateMigrationService: StateMigrationServiceAbstraction
) =>
new StateService(
storageService,
secureStorageService,
logService,
stateMigrationService,
new StateFactory(GlobalState, Account)
),
deps: [ deps: [
StorageServiceAbstraction, StorageServiceAbstraction,
"SECURE_STORAGE", SECURE_STORAGE,
LogService, LogService,
StateMigrationServiceAbstraction, StateMigrationServiceAbstraction,
STATE_FACTORY,
STATE_SERVICE_USE_CACHE,
], ],
}, },
{ {
provide: StateMigrationServiceAbstraction, provide: StateMigrationServiceAbstraction,
useFactory: ( useClass: StateMigrationService,
storageService: StorageServiceAbstraction, deps: [StorageServiceAbstraction, SECURE_STORAGE, STATE_FACTORY],
secureStorageService: StorageServiceAbstraction
) =>
new StateMigrationService(
storageService,
secureStorageService,
new StateFactory(GlobalState, Account)
),
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
}, },
{ {
provide: ExportServiceAbstraction, provide: ExportServiceAbstraction,
@@ -389,33 +341,14 @@ import { ValidationService } from "./validation.service";
}, },
{ {
provide: NotificationsServiceAbstraction, provide: NotificationsServiceAbstraction,
useFactory: ( useClass: NotificationsService,
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
),
deps: [ deps: [
SyncServiceAbstraction, SyncServiceAbstraction,
AppIdServiceAbstraction, AppIdServiceAbstraction,
ApiServiceAbstraction, ApiServiceAbstraction,
VaultTimeoutServiceAbstraction, VaultTimeoutServiceAbstraction,
EnvironmentServiceAbstraction, EnvironmentServiceAbstraction,
MessagingServiceAbstraction, LOGOUT_CALLBACK,
LogService, LogService,
StateServiceAbstraction, StateServiceAbstraction,
], ],
@@ -423,7 +356,7 @@ import { ValidationService } from "./validation.service";
{ {
provide: CryptoFunctionServiceAbstraction, provide: CryptoFunctionServiceAbstraction,
useClass: WebCryptoFunctionService, useClass: WebCryptoFunctionService,
deps: ["WINDOW"], deps: [WINDOW],
}, },
{ {
provide: EventServiceAbstraction, provide: EventServiceAbstraction,

View File

@@ -7,6 +7,10 @@ import { PasswordRepromptComponent } from "../components/password-reprompt.compo
import { ModalService } from "./modal.service"; 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() @Injectable()
export class PasswordRepromptService implements PasswordRepromptServiceAbstraction { export class PasswordRepromptService implements PasswordRepromptServiceAbstraction {
protected component = PasswordRepromptComponent; protected component = PasswordRepromptComponent;

View File

@@ -18,11 +18,9 @@ export class UnauthGuardService implements CanActivate {
if (isAuthed) { if (isAuthed) {
const locked = await this.vaultTimeoutService.isLocked(); const locked = await this.vaultTimeoutService.isLocked();
if (locked) { if (locked) {
this.router.navigate(["lock"]); return this.router.createUrlTree(["lock"]);
} else {
this.router.navigate([this.homepage]);
} }
return false; return this.router.createUrlTree([this.homepage]);
} }
return true; return true;
} }

View File

@@ -7,7 +7,6 @@ module.exports = {
displayName: "common jslib tests", displayName: "common jslib tests",
preset: "ts-jest", preset: "ts-jest",
testEnvironment: "jsdom", testEnvironment: "jsdom",
roots: ["<rootDir>/spec/"],
testMatch: ["**/+(*.)+(spec).+(ts)"], testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"], setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
collectCoverage: true, 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 { DatabaseData } from "./testData/onePassword1Pux/Database";
import { DriversLicenseData } from "./testData/onePassword1Pux/DriversLicense"; import { DriversLicenseData } from "./testData/onePassword1Pux/DriversLicense";
import { EmailAccountData } from "./testData/onePassword1Pux/EmailAccount"; 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 { IdentityData } from "./testData/onePassword1Pux/IdentityData";
import { LoginData } from "./testData/onePassword1Pux/LoginData"; import { LoginData } from "./testData/onePassword1Pux/LoginData";
import { MedicalRecordData } from "./testData/onePassword1Pux/MedicalRecord"; import { MedicalRecordData } from "./testData/onePassword1Pux/MedicalRecord";
@@ -102,6 +105,25 @@ describe("1Password 1Pux Importer", () => {
expect(cipher.fields[1].type).toBe(FieldType.Boolean); 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 () => { it('should create concealed field as "hidden" type', async () => {
const importer = new Importer(); const importer = new Importer();
const result = await importer.parse(OnePuxExampleFileJson); const result = await importer.parse(OnePuxExampleFileJson);
@@ -205,6 +227,46 @@ describe("1Password 1Pux Importer", () => {
validateCustomField(cipher.fields, "forumsig", "super cool guy"); 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 () => { it("should parse category 005 - Password (Legacy)", async () => {
const importer = new Importer(); const importer = new Importer();
const jsonString = JSON.stringify(PasswordData); 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: "", title: "",
id: "irpvnshg5kjpkmj5jwy4xxkfom", id: "irpvnshg5kjpkmj5jwy4xxkfom",
value: { value: {
email: "plexuser@nullvalue.test", email: {
email_address: "plexuser@nullvalue.test",
provider: null,
},
}, },
indexAtSource: 0, indexAtSource: 0,
guarded: false, guarded: false,
@@ -1434,7 +1437,10 @@ export const SanitizedExport: ExportData = {
title: "registered email", title: "registered email",
id: "reg_email", id: "reg_email",
value: { value: {
email: "kriddler@nullvalue.test", email: {
email_address: "kriddler@nullvalue.test",
provider: null,
},
}, },
indexAtSource: 1, indexAtSource: 1,
guarded: false, guarded: false,
@@ -1536,7 +1542,10 @@ export const SanitizedExport: ExportData = {
title: "support email", title: "support email",
id: "support_email", id: "support_email",
value: { value: {
email: "support@nullvalue.test", email: {
email_address: "support@nullvalue.test",
provider: null,
},
}, },
indexAtSource: 4, indexAtSource: 4,
guarded: false, guarded: false,
@@ -4014,7 +4023,10 @@ export const SanitizedExport: ExportData = {
title: "registered email", title: "registered email",
id: "reg_email", id: "reg_email",
value: { value: {
email: "", email: {
email_address: "",
provider: null,
},
}, },
indexAtSource: 1, indexAtSource: 1,
guarded: false, guarded: false,
@@ -4116,7 +4128,10 @@ export const SanitizedExport: ExportData = {
title: "support email", title: "support email",
id: "support_email", id: "support_email",
value: { value: {
email: "", email: {
email_address: "",
provider: null,
},
}, },
indexAtSource: 4, indexAtSource: 4,
guarded: false, guarded: false,

View File

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

View File

@@ -11,7 +11,7 @@ import { Utils } from "jslib-common/misc/utils";
import { Cipher } from "jslib-common/models/domain/cipher"; import { Cipher } from "jslib-common/models/domain/cipher";
import { EncString } from "jslib-common/models/domain/encString"; import { EncString } from "jslib-common/models/domain/encString";
import { Login } from "jslib-common/models/domain/login"; 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 { CipherView } from "jslib-common/models/view/cipherView";
import { LoginView } from "jslib-common/models/view/loginView"; import { LoginView } from "jslib-common/models/view/loginView";
import { ExportService } from "jslib-common/services/export.service"; 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() { function newGuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0; const r = (Math.random() * 16) | 0;
@@ -16,3 +20,18 @@ export function BuildTestObject<T, K extends keyof T = keyof T>(
): T { ): T {
return Object.assign(constructor === null ? {} : new constructor(), def) as 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,6 +0,0 @@
export abstract class BiometricMain {
isError: boolean;
init: () => Promise<void>;
supportsBiometric: () => Promise<boolean>;
authenticateBiometric: () => Promise<boolean>;
}

View File

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

View File

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

View File

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

View File

@@ -258,7 +258,7 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer {
} }
} }
} else if (cipher.type === CipherType.Identity) { } else if (cipher.type === CipherType.Identity) {
if (this.fillIdentity(field, fieldValue, cipher)) { if (this.fillIdentity(field, fieldValue, cipher, valueKey)) {
return; return;
} }
if (valueKey === "address") { 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 // Do not include a password field if it's already in the history
if ( if (
field.title === "password" && field.title === "password" &&
@@ -440,7 +448,12 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer {
return false; 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") { if (this.isNullOrWhitespace(cipher.identity.firstName) && field.id === "firstname") {
cipher.identity.firstName = fieldValue; cipher.identity.firstName = fieldValue;
return true; return true;
@@ -466,9 +479,18 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer {
return true; return true;
} }
if (this.isNullOrWhitespace(cipher.identity.email) && field.id === "email") { if (this.isNullOrWhitespace(cipher.identity.email)) {
cipher.identity.email = fieldValue; if (valueKey === "email") {
return true; 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") { if (this.isNullOrWhitespace(cipher.identity.username) && field.id === "username") {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ export class Login extends Domain {
totp: EncString; totp: EncString;
autofillOnPageLoad: boolean; autofillOnPageLoad: boolean;
constructor(obj?: LoginData, alreadyEncrypted = false) { constructor(obj?: LoginData) {
super(); super();
if (obj == null) { if (obj == null) {
return; return;
@@ -31,14 +31,13 @@ export class Login extends Domain {
password: null, password: null,
totp: null, totp: null,
}, },
alreadyEncrypted,
[] []
); );
if (obj.uris) { if (obj.uris) {
this.uris = []; this.uris = [];
obj.uris.forEach((u) => { 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; uri: EncString;
match: UriMatchType; match: UriMatchType;
constructor(obj?: LoginUriData, alreadyEncrypted = false) { constructor(obj?: LoginUriData) {
super(); super();
if (obj == null) { if (obj == null) {
return; return;
@@ -23,7 +23,6 @@ export class LoginUri extends Domain {
{ {
uri: null, uri: null,
}, },
alreadyEncrypted,
[] []
); );
} }
@@ -46,6 +45,7 @@ export class LoginUri extends Domain {
u, u,
{ {
uri: null, uri: null,
match: null,
}, },
["match"] ["match"]
); );

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ export class SendAccess extends Domain {
expirationDate: Date; expirationDate: Date;
creatorIdentifier: string; creatorIdentifier: string;
constructor(obj?: SendAccessResponse, alreadyEncrypted = false) { constructor(obj?: SendAccessResponse) {
super(); super();
if (obj == null) { if (obj == null) {
return; return;
@@ -32,7 +32,6 @@ export class SendAccess extends Domain {
expirationDate: null, expirationDate: null,
creatorIdentifier: null, creatorIdentifier: null,
}, },
alreadyEncrypted,
["id", "expirationDate", "creatorIdentifier"] ["id", "expirationDate", "creatorIdentifier"]
); );
@@ -40,10 +39,10 @@ export class SendAccess extends Domain {
switch (this.type) { switch (this.type) {
case SendType.Text: case SendType.Text:
this.text = new SendText(obj.text, alreadyEncrypted); this.text = new SendText(obj.text);
break; break;
case SendType.File: case SendType.File:
this.file = new SendFile(obj.file, alreadyEncrypted); this.file = new SendFile(obj.file);
break; break;
default: default:
break; break;

View File

@@ -11,7 +11,7 @@ export class SendFile extends Domain {
sizeName: string; sizeName: string;
fileName: EncString; fileName: EncString;
constructor(obj?: SendFileData, alreadyEncrypted = false) { constructor(obj?: SendFileData) {
super(); super();
if (obj == null) { if (obj == null) {
return; return;
@@ -26,7 +26,6 @@ export class SendFile extends Domain {
sizeName: null, sizeName: null,
fileName: null, fileName: null,
}, },
alreadyEncrypted,
["id", "sizeName"] ["id", "sizeName"]
); );
} }

View File

@@ -9,7 +9,7 @@ export class SendText extends Domain {
text: EncString; text: EncString;
hidden: boolean; hidden: boolean;
constructor(obj?: SendTextData, alreadyEncrypted = false) { constructor(obj?: SendTextData) {
super(); super();
if (obj == null) { if (obj == null) {
return; return;
@@ -22,7 +22,6 @@ export class SendText extends Domain {
{ {
text: null, text: null,
}, },
alreadyEncrypted,
[] []
); );
} }

View File

@@ -2,9 +2,9 @@ import { Card as CardDomain } from "../domain/card";
import { EncString } from "../domain/encString"; import { EncString } from "../domain/encString";
import { CardView } from "../view/cardView"; import { CardView } from "../view/cardView";
export class Card { export class CardExport {
static template(): Card { static template(): CardExport {
const req = new Card(); const req = new CardExport();
req.cardholderName = "John Doe"; req.cardholderName = "John Doe";
req.brand = "visa"; req.brand = "visa";
req.number = "4242424242424242"; req.number = "4242424242424242";
@@ -14,7 +14,7 @@ export class Card {
return req; return req;
} }
static toView(req: Card, view = new CardView()) { static toView(req: CardExport, view = new CardView()) {
view.cardholderName = req.cardholderName; view.cardholderName = req.cardholderName;
view.brand = req.brand; view.brand = req.brand;
view.number = req.number; view.number = req.number;
@@ -24,7 +24,7 @@ export class Card {
return view; return view;
} }
static toDomain(req: Card, domain = new CardDomain()) { static toDomain(req: CardExport, domain = new CardDomain()) {
domain.cardholderName = req.cardholderName != null ? new EncString(req.cardholderName) : null; domain.cardholderName = req.cardholderName != null ? new EncString(req.cardholderName) : null;
domain.brand = req.brand != null ? new EncString(req.brand) : null; domain.brand = req.brand != null ? new EncString(req.brand) : null;
domain.number = req.number != null ? new EncString(req.number) : null; domain.number = req.number != null ? new EncString(req.number) : null;

View File

@@ -4,15 +4,15 @@ import { Cipher as CipherDomain } from "../domain/cipher";
import { EncString } from "../domain/encString"; import { EncString } from "../domain/encString";
import { CipherView } from "../view/cipherView"; import { CipherView } from "../view/cipherView";
import { Card } from "./card"; import { CardExport } from "./cardExport";
import { Field } from "./field"; import { FieldExport } from "./fieldExport";
import { Identity } from "./identity"; import { IdentityExport } from "./identityExport";
import { Login } from "./login"; import { LoginExport } from "./loginExport";
import { SecureNote } from "./secureNote"; import { SecureNoteExport } from "./secureNoteExport";
export class Cipher { export class CipherExport {
static template(): Cipher { static template(): CipherExport {
const req = new Cipher(); const req = new CipherExport();
req.organizationId = null; req.organizationId = null;
req.collectionIds = null; req.collectionIds = null;
req.folderId = null; req.folderId = null;
@@ -29,7 +29,7 @@ export class Cipher {
return req; return req;
} }
static toView(req: Cipher, view = new CipherView()) { static toView(req: CipherExport, view = new CipherView()) {
view.type = req.type; view.type = req.type;
view.folderId = req.folderId; view.folderId = req.folderId;
if (view.organizationId == null) { if (view.organizationId == null) {
@@ -45,28 +45,28 @@ export class Cipher {
view.reprompt = req.reprompt ?? CipherRepromptType.None; view.reprompt = req.reprompt ?? CipherRepromptType.None;
if (req.fields != null) { if (req.fields != null) {
view.fields = req.fields.map((f) => Field.toView(f)); view.fields = req.fields.map((f) => FieldExport.toView(f));
} }
switch (req.type) { switch (req.type) {
case CipherType.Login: case CipherType.Login:
view.login = Login.toView(req.login); view.login = LoginExport.toView(req.login);
break; break;
case CipherType.SecureNote: case CipherType.SecureNote:
view.secureNote = SecureNote.toView(req.secureNote); view.secureNote = SecureNoteExport.toView(req.secureNote);
break; break;
case CipherType.Card: case CipherType.Card:
view.card = Card.toView(req.card); view.card = CardExport.toView(req.card);
break; break;
case CipherType.Identity: case CipherType.Identity:
view.identity = Identity.toView(req.identity); view.identity = IdentityExport.toView(req.identity);
break; break;
} }
return view; return view;
} }
static toDomain(req: Cipher, domain = new CipherDomain()) { static toDomain(req: CipherExport, domain = new CipherDomain()) {
domain.type = req.type; domain.type = req.type;
domain.folderId = req.folderId; domain.folderId = req.folderId;
if (domain.organizationId == null) { if (domain.organizationId == null) {
@@ -78,21 +78,21 @@ export class Cipher {
domain.reprompt = req.reprompt ?? CipherRepromptType.None; domain.reprompt = req.reprompt ?? CipherRepromptType.None;
if (req.fields != null) { if (req.fields != null) {
domain.fields = req.fields.map((f) => Field.toDomain(f)); domain.fields = req.fields.map((f) => FieldExport.toDomain(f));
} }
switch (req.type) { switch (req.type) {
case CipherType.Login: case CipherType.Login:
domain.login = Login.toDomain(req.login); domain.login = LoginExport.toDomain(req.login);
break; break;
case CipherType.SecureNote: case CipherType.SecureNote:
domain.secureNote = SecureNote.toDomain(req.secureNote); domain.secureNote = SecureNoteExport.toDomain(req.secureNote);
break; break;
case CipherType.Card: case CipherType.Card:
domain.card = Card.toDomain(req.card); domain.card = CardExport.toDomain(req.card);
break; break;
case CipherType.Identity: case CipherType.Identity:
domain.identity = Identity.toDomain(req.identity); domain.identity = IdentityExport.toDomain(req.identity);
break; break;
} }
@@ -106,11 +106,11 @@ export class Cipher {
name: string; name: string;
notes: string; notes: string;
favorite: boolean; favorite: boolean;
fields: Field[]; fields: FieldExport[];
login: Login; login: LoginExport;
secureNote: SecureNote; secureNote: SecureNoteExport;
card: Card; card: CardExport;
identity: Identity; identity: IdentityExport;
reprompt: CipherRepromptType; reprompt: CipherRepromptType;
// Use build method instead of ctor so that we can control order of JSON stringify for pretty print // Use build method instead of ctor so that we can control order of JSON stringify for pretty print
@@ -132,24 +132,24 @@ export class Cipher {
if (o.fields != null) { if (o.fields != null) {
if (o instanceof CipherView) { if (o instanceof CipherView) {
this.fields = o.fields.map((f) => new Field(f)); this.fields = o.fields.map((f) => new FieldExport(f));
} else { } else {
this.fields = o.fields.map((f) => new Field(f)); this.fields = o.fields.map((f) => new FieldExport(f));
} }
} }
switch (o.type) { switch (o.type) {
case CipherType.Login: case CipherType.Login:
this.login = new Login(o.login); this.login = new LoginExport(o.login);
break; break;
case CipherType.SecureNote: case CipherType.SecureNote:
this.secureNote = new SecureNote(o.secureNote); this.secureNote = new SecureNoteExport(o.secureNote);
break; break;
case CipherType.Card: case CipherType.Card:
this.card = new Card(o.card); this.card = new CardExport(o.card);
break; break;
case CipherType.Identity: case CipherType.Identity:
this.identity = new Identity(o.identity); this.identity = new IdentityExport(o.identity);
break; break;
} }
} }

View File

@@ -1,9 +1,9 @@
import { Cipher as CipherDomain } from "../domain/cipher"; import { Cipher as CipherDomain } from "../domain/cipher";
import { CipherView } from "../view/cipherView"; import { CipherView } from "../view/cipherView";
import { Cipher } from "./cipher"; import { CipherExport } from "./cipherExport";
export class CipherWithIds extends Cipher { export class CipherWithIdExport extends CipherExport {
id: string; id: string;
collectionIds: string[]; collectionIds: string[];

View File

@@ -2,16 +2,16 @@ import { Collection as CollectionDomain } from "../domain/collection";
import { EncString } from "../domain/encString"; import { EncString } from "../domain/encString";
import { CollectionView } from "../view/collectionView"; import { CollectionView } from "../view/collectionView";
export class Collection { export class CollectionExport {
static template(): Collection { static template(): CollectionExport {
const req = new Collection(); const req = new CollectionExport();
req.organizationId = "00000000-0000-0000-0000-000000000000"; req.organizationId = "00000000-0000-0000-0000-000000000000";
req.name = "Collection name"; req.name = "Collection name";
req.externalId = null; req.externalId = null;
return req; return req;
} }
static toView(req: Collection, view = new CollectionView()) { static toView(req: CollectionExport, view = new CollectionView()) {
view.name = req.name; view.name = req.name;
view.externalId = req.externalId; view.externalId = req.externalId;
if (view.organizationId == null) { if (view.organizationId == null) {
@@ -20,7 +20,7 @@ export class Collection {
return view; return view;
} }
static toDomain(req: Collection, domain = new CollectionDomain()) { static toDomain(req: CollectionExport, domain = new CollectionDomain()) {
domain.name = req.name != null ? new EncString(req.name) : null; domain.name = req.name != null ? new EncString(req.name) : null;
domain.externalId = req.externalId; domain.externalId = req.externalId;
if (domain.organizationId == null) { if (domain.organizationId == null) {

View File

@@ -1,9 +1,9 @@
import { Collection as CollectionDomain } from "../domain/collection"; import { Collection as CollectionDomain } from "../domain/collection";
import { CollectionView } from "../view/collectionView"; import { CollectionView } from "../view/collectionView";
import { Collection } from "./collection"; import { CollectionExport } from "./collectionExport";
export class CollectionWithId extends Collection { export class CollectionWithIdExport extends CollectionExport {
id: string; id: string;
// Use build method instead of ctor so that we can control order of JSON stringify for pretty print // Use build method instead of ctor so that we can control order of JSON stringify for pretty print

View File

@@ -1,7 +1,7 @@
import { EventType } from "../../enums/eventType"; import { EventType } from "../../enums/eventType";
import { EventView } from "../view/eventView"; import { EventView } from "../view/eventView";
export class Event { export class EventExport {
message: string; message: string;
appIcon: string; appIcon: string;
appName: string; appName: string;

View File

@@ -4,16 +4,16 @@ import { EncString } from "../domain/encString";
import { Field as FieldDomain } from "../domain/field"; import { Field as FieldDomain } from "../domain/field";
import { FieldView } from "../view/fieldView"; import { FieldView } from "../view/fieldView";
export class Field { export class FieldExport {
static template(): Field { static template(): FieldExport {
const req = new Field(); const req = new FieldExport();
req.name = "Field name"; req.name = "Field name";
req.value = "Some value"; req.value = "Some value";
req.type = FieldType.Text; req.type = FieldType.Text;
return req; return req;
} }
static toView(req: Field, view = new FieldView()) { static toView(req: FieldExport, view = new FieldView()) {
view.type = req.type; view.type = req.type;
view.value = req.value; view.value = req.value;
view.name = req.name; view.name = req.name;
@@ -21,7 +21,7 @@ export class Field {
return view; return view;
} }
static toDomain(req: Field, domain = new FieldDomain()) { static toDomain(req: FieldExport, domain = new FieldDomain()) {
domain.type = req.type; domain.type = req.type;
domain.value = req.value != null ? new EncString(req.value) : null; domain.value = req.value != null ? new EncString(req.value) : null;
domain.name = req.name != null ? new EncString(req.name) : null; domain.name = req.name != null ? new EncString(req.name) : null;

View File

@@ -2,19 +2,19 @@ import { EncString } from "../domain/encString";
import { Folder as FolderDomain } from "../domain/folder"; import { Folder as FolderDomain } from "../domain/folder";
import { FolderView } from "../view/folderView"; import { FolderView } from "../view/folderView";
export class Folder { export class FolderExport {
static template(): Folder { static template(): FolderExport {
const req = new Folder(); const req = new FolderExport();
req.name = "Folder name"; req.name = "Folder name";
return req; return req;
} }
static toView(req: Folder, view = new FolderView()) { static toView(req: FolderExport, view = new FolderView()) {
view.name = req.name; view.name = req.name;
return view; return view;
} }
static toDomain(req: Folder, domain = new FolderDomain()) { static toDomain(req: FolderExport, domain = new FolderDomain()) {
domain.name = req.name != null ? new EncString(req.name) : null; domain.name = req.name != null ? new EncString(req.name) : null;
return domain; return domain;
} }

View File

@@ -1,9 +1,9 @@
import { Folder as FolderDomain } from "../domain/folder"; import { Folder as FolderDomain } from "../domain/folder";
import { FolderView } from "../view/folderView"; import { FolderView } from "../view/folderView";
import { Folder } from "./folder"; import { FolderExport } from "./folderExport";
export class FolderWithId extends Folder { export class FolderWithIdExport extends FolderExport {
id: string; id: string;
// Use build method instead of ctor so that we can control order of JSON stringify for pretty print // Use build method instead of ctor so that we can control order of JSON stringify for pretty print

View File

@@ -2,9 +2,9 @@ import { EncString } from "../domain/encString";
import { Identity as IdentityDomain } from "../domain/identity"; import { Identity as IdentityDomain } from "../domain/identity";
import { IdentityView } from "../view/identityView"; import { IdentityView } from "../view/identityView";
export class Identity { export class IdentityExport {
static template(): Identity { static template(): IdentityExport {
const req = new Identity(); const req = new IdentityExport();
req.title = "Mr"; req.title = "Mr";
req.firstName = "John"; req.firstName = "John";
req.middleName = "William"; req.middleName = "William";
@@ -26,7 +26,7 @@ export class Identity {
return req; return req;
} }
static toView(req: Identity, view = new IdentityView()) { static toView(req: IdentityExport, view = new IdentityView()) {
view.title = req.title; view.title = req.title;
view.firstName = req.firstName; view.firstName = req.firstName;
view.middleName = req.middleName; view.middleName = req.middleName;
@@ -48,7 +48,7 @@ export class Identity {
return view; return view;
} }
static toDomain(req: Identity, domain = new IdentityDomain()) { static toDomain(req: IdentityExport, domain = new IdentityDomain()) {
domain.title = req.title != null ? new EncString(req.title) : null; domain.title = req.title != null ? new EncString(req.title) : null;
domain.firstName = req.firstName != null ? new EncString(req.firstName) : null; domain.firstName = req.firstName != null ? new EncString(req.firstName) : null;
domain.middleName = req.middleName != null ? new EncString(req.middleName) : null; domain.middleName = req.middleName != null ? new EncString(req.middleName) : null;

View File

@@ -2,11 +2,11 @@ import { EncString } from "../domain/encString";
import { Login as LoginDomain } from "../domain/login"; import { Login as LoginDomain } from "../domain/login";
import { LoginView } from "../view/loginView"; import { LoginView } from "../view/loginView";
import { LoginUri } from "./loginUri"; import { LoginUriExport } from "./loginUriExport";
export class Login { export class LoginExport {
static template(): Login { static template(): LoginExport {
const req = new Login(); const req = new LoginExport();
req.uris = []; req.uris = [];
req.username = "jdoe"; req.username = "jdoe";
req.password = "myp@ssword123"; req.password = "myp@ssword123";
@@ -14,9 +14,9 @@ export class Login {
return req; return req;
} }
static toView(req: Login, view = new LoginView()) { static toView(req: LoginExport, view = new LoginView()) {
if (req.uris != null) { if (req.uris != null) {
view.uris = req.uris.map((u) => LoginUri.toView(u)); view.uris = req.uris.map((u) => LoginUriExport.toView(u));
} }
view.username = req.username; view.username = req.username;
view.password = req.password; view.password = req.password;
@@ -24,9 +24,9 @@ export class Login {
return view; return view;
} }
static toDomain(req: Login, domain = new LoginDomain()) { static toDomain(req: LoginExport, domain = new LoginDomain()) {
if (req.uris != null) { if (req.uris != null) {
domain.uris = req.uris.map((u) => LoginUri.toDomain(u)); domain.uris = req.uris.map((u) => LoginUriExport.toDomain(u));
} }
domain.username = req.username != null ? new EncString(req.username) : null; domain.username = req.username != null ? new EncString(req.username) : null;
domain.password = req.password != null ? new EncString(req.password) : null; domain.password = req.password != null ? new EncString(req.password) : null;
@@ -34,7 +34,7 @@ export class Login {
return domain; return domain;
} }
uris: LoginUri[]; uris: LoginUriExport[];
username: string; username: string;
password: string; password: string;
totp: string; totp: string;
@@ -46,9 +46,9 @@ export class Login {
if (o.uris != null) { if (o.uris != null) {
if (o instanceof LoginView) { if (o instanceof LoginView) {
this.uris = o.uris.map((u) => new LoginUri(u)); this.uris = o.uris.map((u) => new LoginUriExport(u));
} else { } else {
this.uris = o.uris.map((u) => new LoginUri(u)); this.uris = o.uris.map((u) => new LoginUriExport(u));
} }
} }

View File

@@ -3,21 +3,21 @@ import { EncString } from "../domain/encString";
import { LoginUri as LoginUriDomain } from "../domain/loginUri"; import { LoginUri as LoginUriDomain } from "../domain/loginUri";
import { LoginUriView } from "../view/loginUriView"; import { LoginUriView } from "../view/loginUriView";
export class LoginUri { export class LoginUriExport {
static template(): LoginUri { static template(): LoginUriExport {
const req = new LoginUri(); const req = new LoginUriExport();
req.uri = "https://google.com"; req.uri = "https://google.com";
req.match = null; req.match = null;
return req; return req;
} }
static toView(req: LoginUri, view = new LoginUriView()) { static toView(req: LoginUriExport, view = new LoginUriView()) {
view.uri = req.uri; view.uri = req.uri;
view.match = req.match; view.match = req.match;
return view; return view;
} }
static toDomain(req: LoginUri, domain = new LoginUriDomain()) { static toDomain(req: LoginUriExport, domain = new LoginUriDomain()) {
domain.uri = req.uri != null ? new EncString(req.uri) : null; domain.uri = req.uri != null ? new EncString(req.uri) : null;
domain.match = req.match; domain.match = req.match;
return domain; return domain;

View File

@@ -2,19 +2,19 @@ import { SecureNoteType } from "../../enums/secureNoteType";
import { SecureNote as SecureNoteDomain } from "../domain/secureNote"; import { SecureNote as SecureNoteDomain } from "../domain/secureNote";
import { SecureNoteView } from "../view/secureNoteView"; import { SecureNoteView } from "../view/secureNoteView";
export class SecureNote { export class SecureNoteExport {
static template(): SecureNote { static template(): SecureNoteExport {
const req = new SecureNote(); const req = new SecureNoteExport();
req.type = SecureNoteType.Generic; req.type = SecureNoteType.Generic;
return req; return req;
} }
static toView(req: SecureNote, view = new SecureNoteView()) { static toView(req: SecureNoteExport, view = new SecureNoteView()) {
view.type = req.type; view.type = req.type;
return view; return view;
} }
static toDomain(req: SecureNote, view = new SecureNoteDomain()) { static toDomain(req: SecureNoteExport, view = new SecureNoteDomain()) {
view.type = req.type; view.type = req.type;
return view; return view;
} }

View File

@@ -2,4 +2,5 @@ import { SecretVerificationRequest } from "./secretVerificationRequest";
export class TwoFactorEmailRequest extends SecretVerificationRequest { export class TwoFactorEmailRequest extends SecretVerificationRequest {
email: string; email: string;
deviceIdentifier: string;
} }

View File

@@ -306,7 +306,7 @@ export class CipherService implements CipherServiceAbstraction {
} }
const localData = await this.stateService.getLocalData(); const localData = await this.stateService.getLocalData();
return new Cipher(ciphers[id], false, localData ? localData[id] : null); return new Cipher(ciphers[id], localData ? localData[id] : null);
} }
async getAll(): Promise<Cipher[]> { async getAll(): Promise<Cipher[]> {
@@ -316,7 +316,7 @@ export class CipherService implements CipherServiceAbstraction {
for (const id in ciphers) { for (const id in ciphers) {
// eslint-disable-next-line // eslint-disable-next-line
if (ciphers.hasOwnProperty(id)) { if (ciphers.hasOwnProperty(id)) {
response.push(new Cipher(ciphers[id], false, localData ? localData[id] : null)); response.push(new Cipher(ciphers[id], localData ? localData[id] : null));
} }
} }
return response; return response;
@@ -605,11 +605,7 @@ export class CipherService implements CipherServiceAbstraction {
response = await this.apiService.putCipher(cipher.id, request); response = await this.apiService.putCipher(cipher.id, request);
} }
const data = new CipherData( const data = new CipherData(response, cipher.collectionIds);
response,
await this.stateService.getUserId(),
cipher.collectionIds
);
await this.upsert(data); await this.upsert(data);
} }
@@ -635,7 +631,7 @@ export class CipherService implements CipherServiceAbstraction {
const encCipher = await this.encrypt(cipher); const encCipher = await this.encrypt(cipher);
const request = new CipherShareRequest(encCipher); const request = new CipherShareRequest(encCipher);
const response = await this.apiService.putShareCipher(cipher.id, request); const response = await this.apiService.putShareCipher(cipher.id, request);
const data = new CipherData(response, await this.stateService.getUserId(), collectionIds); const data = new CipherData(response, collectionIds);
await this.upsert(data); await this.upsert(data);
} }
@@ -666,8 +662,7 @@ export class CipherService implements CipherServiceAbstraction {
} }
throw e; throw e;
} }
const userId = await this.stateService.getUserId(); await this.upsert(encCiphers.map((c) => c.toCipherData()));
await this.upsert(encCiphers.map((c) => c.toCipherData(userId)));
} }
saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any, admin = false): Promise<Cipher> { saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any, admin = false): Promise<Cipher> {
@@ -741,11 +736,7 @@ export class CipherService implements CipherServiceAbstraction {
} }
} }
const cData = new CipherData( const cData = new CipherData(response, cipher.collectionIds);
response,
await this.stateService.getUserId(),
cipher.collectionIds
);
if (!admin) { if (!admin) {
await this.upsert(cData); await this.upsert(cData);
} }
@@ -801,7 +792,7 @@ export class CipherService implements CipherServiceAbstraction {
async saveCollectionsWithServer(cipher: Cipher): Promise<any> { async saveCollectionsWithServer(cipher: Cipher): Promise<any> {
const request = new CipherCollectionsRequest(cipher.collectionIds); const request = new CipherCollectionsRequest(cipher.collectionIds);
await this.apiService.putCipherCollections(cipher.id, request); await this.apiService.putCipherCollections(cipher.id, request);
const data = cipher.toCipherData(await this.stateService.getUserId()); const data = cipher.toCipherData();
await this.upsert(data); await this.upsert(data);
} }

View File

@@ -17,10 +17,10 @@ import { CollectionData } from "../models/data/collectionData";
import { Cipher } from "../models/domain/cipher"; import { Cipher } from "../models/domain/cipher";
import { Collection } from "../models/domain/collection"; import { Collection } from "../models/domain/collection";
import { Folder } from "../models/domain/folder"; import { Folder } from "../models/domain/folder";
import { CipherWithIds as CipherExport } from "../models/export/cipherWithIds"; import { CipherWithIdExport as CipherExport } from "../models/export/cipherWithIdsExport";
import { CollectionWithId as CollectionExport } from "../models/export/collectionWithId"; import { CollectionWithIdExport as CollectionExport } from "../models/export/collectionWithIdExport";
import { Event } from "../models/export/event"; import { EventExport } from "../models/export/eventExport";
import { FolderWithId as FolderExport } from "../models/export/folderWithId"; import { FolderWithIdExport as FolderExport } from "../models/export/folderWithIdExport";
import { CollectionDetailsResponse } from "../models/response/collectionResponse"; import { CollectionDetailsResponse } from "../models/response/collectionResponse";
import { CipherView } from "../models/view/cipherView"; import { CipherView } from "../models/view/cipherView";
import { CollectionView } from "../models/view/collectionView"; import { CollectionView } from "../models/view/collectionView";
@@ -90,7 +90,7 @@ export class ExportService implements ExportServiceAbstraction {
} }
async getEventExport(events: EventView[]): Promise<string> { async getEventExport(events: EventView[]): Promise<string> {
return papa.unparse(events.map((e) => new Event(e))); return papa.unparse(events.map((e) => new EventExport(e)));
} }
getFileName(prefix: string = null, extension = "csv"): string { getFileName(prefix: string = null, extension = "csv"): string {

View File

@@ -117,8 +117,7 @@ export class FolderService implements FolderServiceAbstraction {
response = await this.apiService.putFolder(folder.id, request); response = await this.apiService.putFolder(folder.id, request);
} }
const userId = await this.stateService.getUserId(); const data = new FolderData(response);
const data = new FolderData(response, userId);
await this.upsert(data); await this.upsert(data);
} }

View File

@@ -31,7 +31,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
private apiService: ApiService, private apiService: ApiService,
private vaultTimeoutService: VaultTimeoutService, private vaultTimeoutService: VaultTimeoutService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private logoutCallback: () => Promise<void>, private logoutCallback: (expired: boolean) => Promise<void>,
private logService: LogService, private logService: LogService,
private stateService: StateService private stateService: StateService
) { ) {
@@ -169,7 +169,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
break; break;
case NotificationType.LogOut: case NotificationType.LogOut:
if (isAuthenticated) { if (isAuthenticated) {
this.logoutCallback(); this.logoutCallback(true);
} }
break; break;
case NotificationType.SyncSendCreate: case NotificationType.SyncSendCreate:

View File

@@ -180,7 +180,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> { async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> {
let options = await this.stateService.getPasswordGenerationOptions(); let options = await this.stateService.getPasswordGenerationOptions();
if (options == null) { if (options == null) {
options = DefaultOptions; options = Object.assign({}, DefaultOptions);
} else { } else {
options = Object.assign({}, DefaultOptions, options); options = Object.assign({}, DefaultOptions, options);
} }

View File

@@ -166,8 +166,7 @@ export class SendService implements SendServiceAbstraction {
response = await this.apiService.putSend(sendData[0].id, request); response = await this.apiService.putSend(sendData[0].id, request);
} }
const userId = await this.stateService.getUserId(); const data = new SendData(response);
const data = new SendData(response, userId);
await this.upsert(data); await this.upsert(data);
} }
@@ -257,8 +256,7 @@ export class SendService implements SendServiceAbstraction {
async removePasswordWithServer(id: string): Promise<any> { async removePasswordWithServer(id: string): Promise<any> {
const response = await this.apiService.putSendRemovePassword(id); const response = await this.apiService.putSendRemovePassword(id);
const userId = await this.stateService.getUserId(); const data = new SendData(response);
const data = new SendData(response, userId);
await this.upsert(data); await this.upsert(data);
} }

View File

@@ -1633,19 +1633,6 @@ export class StateService<
); );
} }
async getLoginRedirect(options?: StorageOptions): Promise<any> {
return (await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions)))
?.loginRedirect;
}
async setLoginRedirect(value: any, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, this.defaultInMemoryOptions)
);
globals.loginRedirect = value;
await this.saveGlobals(globals, this.reconcileOptions(options, this.defaultInMemoryOptions));
}
async getMainWindowSize(options?: StorageOptions): Promise<number> { async getMainWindowSize(options?: StorageOptions): Promise<number> {
return (await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions))) return (await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions)))
?.mainWindowSize; ?.mainWindowSize;

View File

@@ -95,16 +95,15 @@ export class SyncService implements SyncServiceAbstraction {
return this.syncCompleted(false); return this.syncCompleted(false);
} }
const userId = await this.stateService.getUserId();
try { try {
await this.apiService.refreshIdentityToken(); await this.apiService.refreshIdentityToken();
const response = await this.apiService.getSync(); const response = await this.apiService.getSync();
await this.syncProfile(response.profile); await this.syncProfile(response.profile);
await this.syncFolders(userId, response.folders); await this.syncFolders(response.folders);
await this.syncCollections(response.collections); await this.syncCollections(response.collections);
await this.syncCiphers(userId, response.ciphers); await this.syncCiphers(response.ciphers);
await this.syncSends(userId, response.sends); await this.syncSends(response.sends);
await this.syncSettings(response.domains); await this.syncSettings(response.domains);
await this.syncPolicies(response.policies); await this.syncPolicies(response.policies);
@@ -130,8 +129,7 @@ export class SyncService implements SyncServiceAbstraction {
) { ) {
const remoteFolder = await this.apiService.getFolder(notification.id); const remoteFolder = await this.apiService.getFolder(notification.id);
if (remoteFolder != null) { if (remoteFolder != null) {
const userId = await this.stateService.getUserId(); await this.folderService.upsert(new FolderData(remoteFolder));
await this.folderService.upsert(new FolderData(remoteFolder, userId));
this.messagingService.send("syncedUpsertedFolder", { folderId: notification.id }); this.messagingService.send("syncedUpsertedFolder", { folderId: notification.id });
return this.syncCompleted(true); return this.syncCompleted(true);
} }
@@ -200,8 +198,7 @@ export class SyncService implements SyncServiceAbstraction {
if (shouldUpdate) { if (shouldUpdate) {
const remoteCipher = await this.apiService.getCipher(notification.id); const remoteCipher = await this.apiService.getCipher(notification.id);
if (remoteCipher != null) { if (remoteCipher != null) {
const userId = await this.stateService.getUserId(); await this.cipherService.upsert(new CipherData(remoteCipher));
await this.cipherService.upsert(new CipherData(remoteCipher, userId));
this.messagingService.send("syncedUpsertedCipher", { cipherId: notification.id }); this.messagingService.send("syncedUpsertedCipher", { cipherId: notification.id });
return this.syncCompleted(true); return this.syncCompleted(true);
} }
@@ -238,8 +235,7 @@ export class SyncService implements SyncServiceAbstraction {
) { ) {
const remoteSend = await this.apiService.getSend(notification.id); const remoteSend = await this.apiService.getSend(notification.id);
if (remoteSend != null) { if (remoteSend != null) {
const userId = await this.stateService.getUserId(); await this.sendService.upsert(new SendData(remoteSend));
await this.sendService.upsert(new SendData(remoteSend, userId));
this.messagingService.send("syncedUpsertedSend", { sendId: notification.id }); this.messagingService.send("syncedUpsertedSend", { sendId: notification.id });
return this.syncCompleted(true); return this.syncCompleted(true);
} }
@@ -339,10 +335,10 @@ export class SyncService implements SyncServiceAbstraction {
} }
} }
private async syncFolders(userId: string, response: FolderResponse[]) { private async syncFolders(response: FolderResponse[]) {
const folders: { [id: string]: FolderData } = {}; const folders: { [id: string]: FolderData } = {};
response.forEach((f) => { response.forEach((f) => {
folders[f.id] = new FolderData(f, userId); folders[f.id] = new FolderData(f);
}); });
return await this.folderService.replace(folders); return await this.folderService.replace(folders);
} }
@@ -355,18 +351,18 @@ export class SyncService implements SyncServiceAbstraction {
return await this.collectionService.replace(collections); return await this.collectionService.replace(collections);
} }
private async syncCiphers(userId: string, response: CipherResponse[]) { private async syncCiphers(response: CipherResponse[]) {
const ciphers: { [id: string]: CipherData } = {}; const ciphers: { [id: string]: CipherData } = {};
response.forEach((c) => { response.forEach((c) => {
ciphers[c.id] = new CipherData(c, userId); ciphers[c.id] = new CipherData(c);
}); });
return await this.cipherService.replace(ciphers); return await this.cipherService.replace(ciphers);
} }
private async syncSends(userId: string, response: SendResponse[]) { private async syncSends(response: SendResponse[]) {
const sends: { [id: string]: SendData } = {}; const sends: { [id: string]: SendData } = {};
response.forEach((s) => { response.forEach((s) => {
sends[s.id] = new SendData(s, userId); sends[s.id] = new SendData();
}); });
return await this.sendService.replace(sends); return await this.sendService.replace(sends);
} }

View File

@@ -7,6 +7,10 @@ import { VerifyOTPRequest } from "../models/request/account/verifyOTPRequest";
import { SecretVerificationRequest } from "../models/request/secretVerificationRequest"; import { SecretVerificationRequest } from "../models/request/secretVerificationRequest";
import { Verification } from "../types/verification"; import { Verification } from "../types/verification";
/**
* Used for general-purpose user verification throughout the app.
* Use it to verify the input collected by UserVerificationComponent.
*/
export class UserVerificationService implements UserVerificationServiceAbstraction { export class UserVerificationService implements UserVerificationServiceAbstraction {
constructor( constructor(
private cryptoService: CryptoService, private cryptoService: CryptoService,
@@ -14,6 +18,12 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
private apiService: ApiService private apiService: ApiService
) {} ) {}
/**
* Create a new request model to be used for server-side verification
* @param verification User-supplied verification data (Master Password or OTP)
* @param requestClass The request model to create
* @param alreadyHashed Whether the master password is already hashed
*/
async buildRequest<T extends SecretVerificationRequest>( async buildRequest<T extends SecretVerificationRequest>(
verification: Verification, verification: Verification,
requestClass?: new () => T, requestClass?: new () => T,
@@ -35,6 +45,11 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
return request; return request;
} }
/**
* Used to verify the Master Password client-side, or send the OTP to the server for verification (with no other data)
* Generally used for client-side verification only.
* @param verification User-supplied verification data (Master Password or OTP)
*/
async verifyUser(verification: Verification): Promise<boolean> { async verifyUser(verification: Verification): Promise<boolean> {
this.validateInput(verification); this.validateInput(verification);

View File

@@ -1,3 +1,4 @@
import { ApiService } from "../abstractions/api.service";
import { CryptoService } from "../abstractions/crypto.service"; import { CryptoService } from "../abstractions/crypto.service";
import { StateService } from "../abstractions/state.service"; import { StateService } from "../abstractions/state.service";
import { UsernameGenerationService as BaseUsernameGenerationService } from "../abstractions/usernameGeneration.service"; import { UsernameGenerationService as BaseUsernameGenerationService } from "../abstractions/usernameGeneration.service";
@@ -9,10 +10,16 @@ const DefaultOptions = {
wordIncludeNumber: true, wordIncludeNumber: true,
subaddressType: "random", subaddressType: "random",
catchallType: "random", catchallType: "random",
forwardedType: "simplelogin",
forwardedAnonAddyDomain: "anonaddy.me",
}; };
export class UsernameGenerationService implements BaseUsernameGenerationService { export class UsernameGenerationService implements BaseUsernameGenerationService {
constructor(private cryptoService: CryptoService, private stateService: StateService) {} constructor(
private cryptoService: CryptoService,
private stateService: StateService,
private apiService: ApiService
) {}
generateUsername(options: any): Promise<string> { generateUsername(options: any): Promise<string> {
if (options.type === "catchall") { if (options.type === "catchall") {
@@ -20,7 +27,7 @@ export class UsernameGenerationService implements BaseUsernameGenerationService
} else if (options.type === "subaddress") { } else if (options.type === "subaddress") {
return this.generateSubaddress(options); return this.generateSubaddress(options);
} else if (options.type === "forwarded") { } else if (options.type === "forwarded") {
return this.generateSubaddress(options); return this.generateForwarded(options);
} else { } else {
return this.generateWord(options); return this.generateWord(options);
} }
@@ -94,10 +101,50 @@ export class UsernameGenerationService implements BaseUsernameGenerationService
return startString + "@" + o.catchallDomain; return startString + "@" + o.catchallDomain;
} }
async generateForwarded(options: any): Promise<string> {
const o = Object.assign({}, DefaultOptions, options);
if (o.forwardedService == null) {
return null;
}
if (o.forwardedService === "simplelogin") {
if (o.forwardedSimpleLoginApiKey == null || o.forwardedSimpleLoginApiKey === "") {
return null;
}
return this.generateSimpleLoginAlias(
o.forwardedSimpleLoginApiKey,
o.forwardedSimpleLoginHostname,
o.website
);
} else if (o.forwardedService === "anonaddy") {
if (
o.forwardedAnonAddyApiToken == null ||
o.forwardedAnonAddyApiToken === "" ||
o.forwardedAnonAddyDomain == null ||
o.forwardedAnonAddyDomain == ""
) {
return null;
}
return this.generateAnonAddyAlias(
o.forwardedAnonAddyApiToken,
o.forwardedAnonAddyDomain,
o.website
);
} else if (o.forwardedService === "firefoxrelay") {
if (o.forwardedFirefoxApiToken == null || o.forwardedFirefoxApiToken === "") {
return null;
}
return this.generateFirefoxRelayAlias(o.forwardedFirefoxApiToken, o.website);
}
return null;
}
async getOptions(): Promise<any> { async getOptions(): Promise<any> {
let options = await this.stateService.getUsernameGenerationOptions(); let options = await this.stateService.getUsernameGenerationOptions();
if (options == null) { if (options == null) {
options = DefaultOptions; options = Object.assign({}, DefaultOptions);
} else { } else {
options = Object.assign({}, DefaultOptions, options); options = Object.assign({}, DefaultOptions, options);
} }
@@ -125,4 +172,112 @@ export class UsernameGenerationService implements BaseUsernameGenerationService
? number ? number
: new Array(width - number.length + 1).join("0") + number; : new Array(width - number.length + 1).join("0") + number;
} }
private async generateSimpleLoginAlias(
apiKey: string,
hostname: string,
websiteNote: string
): Promise<string> {
if (apiKey == null || apiKey === "") {
throw "Invalid SimpleLogin API key.";
}
const requestInit: RequestInit = {
cache: "no-store",
method: "POST",
headers: new Headers({
Authentication: apiKey,
"Content-Type": "application/json",
}),
};
let url = "https://app.simplelogin.io/api/alias/random/new";
if (hostname != null) {
url += "?hostname=" + hostname;
}
requestInit.body = JSON.stringify({
note:
(websiteNote != null ? "Website: " + websiteNote + ". " : "") + "Generated by Bitwarden.",
});
const request = new Request(url, requestInit);
const response = await this.apiService.nativeFetch(request);
if (response.status === 200 || response.status === 201) {
const json = await response.json();
return json.alias;
}
if (response.status === 401) {
throw "Invalid SimpleLogin API key.";
}
try {
const json = await response.json();
if (json?.error != null) {
throw "SimpleLogin error:" + json.error;
}
} catch {
// Do nothing...
}
throw "Unknown SimpleLogin error occurred.";
}
private async generateAnonAddyAlias(
apiToken: string,
domain: string,
websiteNote: string
): Promise<string> {
if (apiToken == null || apiToken === "") {
throw "Invalid AnonAddy API token.";
}
const requestInit: RequestInit = {
cache: "no-store",
method: "POST",
headers: new Headers({
Authorization: "Bearer " + apiToken,
"Content-Type": "application/json",
}),
};
const url = "https://app.anonaddy.com/api/v1/aliases";
requestInit.body = JSON.stringify({
domain: domain,
description:
(websiteNote != null ? "Website: " + websiteNote + ". " : "") + "Generated by Bitwarden.",
});
const request = new Request(url, requestInit);
const response = await this.apiService.nativeFetch(request);
if (response.status === 200 || response.status === 201) {
const json = await response.json();
return json?.data?.email;
}
if (response.status === 401) {
throw "Invalid AnonAddy API token.";
}
throw "Unknown AnonAddy error occurred.";
}
private async generateFirefoxRelayAlias(apiToken: string, website: string): Promise<string> {
if (apiToken == null || apiToken === "") {
throw "Invalid Firefox Relay API token.";
}
const requestInit: RequestInit = {
cache: "no-store",
method: "POST",
headers: new Headers({
Authorization: "Token " + apiToken,
"Content-Type": "application/json",
}),
};
const url = "https://relay.firefox.com/api/v1/relayaddresses/";
requestInit.body = JSON.stringify({
enabled: true,
generated_for: website,
description: (website != null ? website + " - " : "") + "Generated by Bitwarden.",
});
const request = new Request(url, requestInit);
const response = await this.apiService.nativeFetch(request);
if (response.status === 200 || response.status === 201) {
const json = await response.json();
return json?.full_address;
}
if (response.status === 401) {
throw "Invalid Firefox Relay API token.";
}
throw "Unknown Firefox Relay error occurred.";
}
} }

View File

@@ -29,7 +29,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
private keyConnectorService: KeyConnectorService, private keyConnectorService: KeyConnectorService,
private stateService: StateService, private stateService: StateService,
private lockedCallback: (userId?: string) => Promise<void> = null, private lockedCallback: (userId?: string) => Promise<void> = null,
private loggedOutCallback: (userId?: string) => Promise<void> = null private loggedOutCallback: (expired: boolean, userId?: string) => Promise<void> = null
) {} ) {}
init(checkOnInterval: boolean) { init(checkOnInterval: boolean) {
@@ -116,7 +116,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
async logOut(userId?: string): Promise<void> { async logOut(userId?: string): Promise<void> {
if (this.loggedOutCallback != null) { if (this.loggedOutCallback != null) {
await this.loggedOutCallback(userId); await this.loggedOutCallback(false, userId);
} }
} }

View File

@@ -1,6 +1,11 @@
module.exports = { module.exports = {
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-a11y"], addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-a11y",
"storybook-addon-designs",
],
framework: "@storybook/angular", framework: "@storybook/angular",
core: { core: {
builder: "webpack5", builder: "webpack5",

View File

@@ -47,6 +47,7 @@
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0", "karma-jasmine-html-reporter": "~1.7.0",
"postcss": "^8.4.6", "postcss": "^8.4.6",
"storybook-addon-designs": "^6.2.1",
"tailwindcss": "^3.0.18", "tailwindcss": "^3.0.18",
"typescript": "~4.3.5" "typescript": "~4.3.5"
} }
@@ -4036,6 +4037,29 @@
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==", "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==",
"dev": true "dev": true
}, },
"node_modules/@figspec/components": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@figspec/components/-/components-1.0.0.tgz",
"integrity": "sha512-a8sgP0YLJ3H0g0pdZPYecxfp9JNVQUTaaU3xcSci8duHXTGkJ7X8QPPCBbyhB+MoxMxnsAh8GjkfZHEr9oIoPQ==",
"dev": true,
"dependencies": {
"copy-to-clipboard": "^3.0.0",
"lit-element": "^2.4.0",
"lit-html": "^1.1.1"
}
},
"node_modules/@figspec/react": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@figspec/react/-/react-1.0.0.tgz",
"integrity": "sha512-BkOu3RsKF5vCtPoqsc6Oeyxw4wr9GesFrB9/wDHFqgjzhWsw8erFxCsPxsjdlJD8d8OWVHoM6SWxAaGe/pLdxg==",
"dev": true,
"dependencies": {
"@figspec/components": "^1.0.0"
},
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0"
}
},
"node_modules/@foliojs-fork/fontkit": { "node_modules/@foliojs-fork/fontkit": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/@foliojs-fork/fontkit/-/fontkit-1.9.1.tgz", "resolved": "https://registry.npmjs.org/@foliojs-fork/fontkit/-/fontkit-1.9.1.tgz",
@@ -22451,6 +22475,21 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true "dev": true
}, },
"node_modules/lit-element": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.5.1.tgz",
"integrity": "sha512-ogu7PiJTA33bEK0xGu1dmaX5vhcRjBXCFexPja0e7P7jqLhTpNKYRPmE+GmiCaRVAbiQKGkUgkh/i6+bh++dPQ==",
"dev": true,
"dependencies": {
"lit-html": "^1.1.1"
}
},
"node_modules/lit-html": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.4.1.tgz",
"integrity": "sha512-B9btcSgPYb1q4oSOb/PrOT6Z/H+r6xuNzfH4lFli/AWhYwdtrgQkQWBbIc6mdnf6E2IL3gDXdkkqNktpU0OZQA==",
"dev": true
},
"node_modules/loader-runner": { "node_modules/loader-runner": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz",
@@ -30454,6 +30493,15 @@
"integrity": "sha512-iJtHSGmNgAUx0b/MCS6ASGxb//hGrHHRgzvN+K5bvkBTN7A9RTpPSf1WSp+nPGvWCJ1jRnvY7MKnuqfoi3OEqg==", "integrity": "sha512-iJtHSGmNgAUx0b/MCS6ASGxb//hGrHHRgzvN+K5bvkBTN7A9RTpPSf1WSp+nPGvWCJ1jRnvY7MKnuqfoi3OEqg==",
"dev": true "dev": true
}, },
"node_modules/storybook-addon-designs": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/storybook-addon-designs/-/storybook-addon-designs-6.2.1.tgz",
"integrity": "sha512-ihsscab8185HnxqTNZlM4TfrCPVsO7AimVA8BapuqT/sfZQF9m5H9C0plT3kbECdIMh2cmzMBF1Tc9ckWRgpWg==",
"dev": true,
"dependencies": {
"@figspec/react": "^1.0.0"
}
},
"node_modules/stream-browserify": { "node_modules/stream-browserify": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
@@ -37238,6 +37286,26 @@
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==", "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==",
"dev": true "dev": true
}, },
"@figspec/components": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@figspec/components/-/components-1.0.0.tgz",
"integrity": "sha512-a8sgP0YLJ3H0g0pdZPYecxfp9JNVQUTaaU3xcSci8duHXTGkJ7X8QPPCBbyhB+MoxMxnsAh8GjkfZHEr9oIoPQ==",
"dev": true,
"requires": {
"copy-to-clipboard": "^3.0.0",
"lit-element": "^2.4.0",
"lit-html": "^1.1.1"
}
},
"@figspec/react": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@figspec/react/-/react-1.0.0.tgz",
"integrity": "sha512-BkOu3RsKF5vCtPoqsc6Oeyxw4wr9GesFrB9/wDHFqgjzhWsw8erFxCsPxsjdlJD8d8OWVHoM6SWxAaGe/pLdxg==",
"dev": true,
"requires": {
"@figspec/components": "^1.0.0"
}
},
"@foliojs-fork/fontkit": { "@foliojs-fork/fontkit": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/@foliojs-fork/fontkit/-/fontkit-1.9.1.tgz", "resolved": "https://registry.npmjs.org/@foliojs-fork/fontkit/-/fontkit-1.9.1.tgz",
@@ -51668,6 +51736,21 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true "dev": true
}, },
"lit-element": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.5.1.tgz",
"integrity": "sha512-ogu7PiJTA33bEK0xGu1dmaX5vhcRjBXCFexPja0e7P7jqLhTpNKYRPmE+GmiCaRVAbiQKGkUgkh/i6+bh++dPQ==",
"dev": true,
"requires": {
"lit-html": "^1.1.1"
}
},
"lit-html": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.4.1.tgz",
"integrity": "sha512-B9btcSgPYb1q4oSOb/PrOT6Z/H+r6xuNzfH4lFli/AWhYwdtrgQkQWBbIc6mdnf6E2IL3gDXdkkqNktpU0OZQA==",
"dev": true
},
"loader-runner": { "loader-runner": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz",
@@ -57893,6 +57976,15 @@
"integrity": "sha512-iJtHSGmNgAUx0b/MCS6ASGxb//hGrHHRgzvN+K5bvkBTN7A9RTpPSf1WSp+nPGvWCJ1jRnvY7MKnuqfoi3OEqg==", "integrity": "sha512-iJtHSGmNgAUx0b/MCS6ASGxb//hGrHHRgzvN+K5bvkBTN7A9RTpPSf1WSp+nPGvWCJ1jRnvY7MKnuqfoi3OEqg==",
"dev": true "dev": true
}, },
"storybook-addon-designs": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/storybook-addon-designs/-/storybook-addon-designs-6.2.1.tgz",
"integrity": "sha512-ihsscab8185HnxqTNZlM4TfrCPVsO7AimVA8BapuqT/sfZQF9m5H9C0plT3kbECdIMh2cmzMBF1Tc9ckWRgpWg==",
"dev": true,
"requires": {
"@figspec/react": "^1.0.0"
}
},
"stream-browserify": { "stream-browserify": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",

View File

@@ -53,6 +53,7 @@
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0", "karma-jasmine-html-reporter": "~1.7.0",
"postcss": "^8.4.6", "postcss": "^8.4.6",
"storybook-addon-designs": "^6.2.1",
"tailwindcss": "^3.0.18", "tailwindcss": "^3.0.18",
"typescript": "~4.3.5" "typescript": "~4.3.5"
} }

View File

@@ -8,6 +8,12 @@ export default {
args: { args: {
type: "primary", type: "primary",
}, },
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A16956",
},
},
} as Meta; } as Meta;
const Template: Story<BadgeComponent> = (args: BadgeComponent) => ({ const Template: Story<BadgeComponent> = (args: BadgeComponent) => ({

View File

@@ -0,0 +1,14 @@
<div
class="tw-py-2.5 tw-px-4 tw-text-contrast tw-flex tw-gap-2 tw-items-center"
[ngClass]="bannerClass"
[attr.role]="useAlertRole ? 'status' : null"
[attr.aria-live]="useAlertRole ? 'polite' : null"
>
<i class="bwi tw-align-middle" [ngClass]="icon" *ngIf="icon" aria-hidden="true"></i>
<span class="tw-text-base tw-grow">
<ng-content></ng-content>
</span>
<button class="tw-border-0 tw-bg-transparent tw-text-contrast tw-p-0" (click)="onClose.emit()">
<i class="bwi bwi-close tw-text-sm" *ngIf="icon" aria-hidden="true"></i>
</button>
</div>

View File

@@ -0,0 +1,35 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { BannerComponent } from "./banner.component";
describe("BannerComponent", () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [BannerComponent],
}).compileComponents();
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create with alert", () => {
expect(component.useAlertRole).toBeTrue();
const el = fixture.nativeElement.children[0];
expect(el.getAttribute("role")).toEqual("status");
expect(el.getAttribute("aria-live")).toEqual("polite");
});
it("useAlertRole=false", () => {
component.useAlertRole = false;
fixture.autoDetectChanges();
expect(component.useAlertRole).toBeFalse();
const el = fixture.nativeElement.children[0];
expect(el.getAttribute("role")).toBeNull();
expect(el.getAttribute("aria-live")).toBeNull();
});
});

View File

@@ -0,0 +1,39 @@
import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core";
type BannerTypes = "premium" | "info" | "warning" | "danger";
const defaultIcon: Record<BannerTypes, string> = {
premium: "bwi-star",
info: "bwi-info-circle",
warning: "bwi-exclamation-triangle",
danger: "bwi-error",
};
@Component({
selector: "bit-banner",
templateUrl: "./banner.component.html",
})
export class BannerComponent implements OnInit {
@Input("bannerType") bannerType: BannerTypes = "info";
@Input() icon: string;
@Input() useAlertRole = true;
@Output() onClose = new EventEmitter<void>();
ngOnInit(): void {
this.icon ??= defaultIcon[this.bannerType];
}
get bannerClass() {
switch (this.bannerType) {
case "danger":
return "tw-bg-danger-500";
case "info":
return "tw-bg-info-500";
case "premium":
return "tw-bg-success-500";
case "warning":
return "tw-bg-warning-500";
}
}
}

View File

@@ -0,0 +1,11 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { BannerComponent } from "./banner.component";
@NgModule({
imports: [CommonModule],
exports: [BannerComponent],
declarations: [BannerComponent],
})
export class BannerModule {}

View File

@@ -0,0 +1,38 @@
import { Meta, Story } from "@storybook/angular";
import { BannerComponent } from "./banner.component";
export default {
title: "Jslib/Banner",
component: BannerComponent,
args: {
bannerType: "warning",
},
} as Meta;
const Template: Story<BannerComponent> = (args: BannerComponent) => ({
props: args,
template: `
<bit-banner [bannerType]="bannerType">Content Really Long Text Lorem Ipsum Ipsum Ipsum <button>Button</button></bit-banner>
`,
});
export const Premium = Template.bind({});
Premium.args = {
bannerType: "premium",
};
export const Info = Template.bind({});
Info.args = {
bannerType: "info",
};
export const Warning = Template.bind({});
Warning.args = {
bannerType: "warning",
};
export const Danger = Template.bind({});
Danger.args = {
bannerType: "danger",
};

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