mirror of
https://github.com/bitwarden/browser
synced 2025-12-22 11:13:46 +00:00
Merge branch 'EC-598-beeep-properly-store-passkeys-in-bitwarden' into PM-2207
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<label class="environment-selector-btn">
|
||||
{{ "region" | i18n }}:
|
||||
{{ "loggingInOn" | i18n }}:
|
||||
<a
|
||||
(click)="toggle(null)"
|
||||
cdkOverlayOrigin
|
||||
@@ -8,8 +8,12 @@
|
||||
aria-controls="cdk-overlay-container"
|
||||
[ngSwitch]="selectedEnvironment"
|
||||
>
|
||||
<label *ngSwitchCase="ServerEnvironmentType.US" class="text-primary">{{ "us" | i18n }}</label>
|
||||
<label *ngSwitchCase="ServerEnvironmentType.EU" class="text-primary">{{ "eu" | i18n }}</label>
|
||||
<label *ngSwitchCase="ServerEnvironmentType.US" class="text-primary">{{
|
||||
"usDomain" | i18n
|
||||
}}</label>
|
||||
<label *ngSwitchCase="ServerEnvironmentType.EU" class="text-primary">{{
|
||||
"euDomain" | i18n
|
||||
}}</label>
|
||||
<label *ngSwitchCase="ServerEnvironmentType.SelfHosted" class="text-primary">{{
|
||||
"selfHosted" | i18n
|
||||
}}</label>
|
||||
@@ -23,7 +27,7 @@
|
||||
(backdropClick)="close()"
|
||||
(detach)="close()"
|
||||
[cdkConnectedOverlayOpen]="isOpen"
|
||||
[cdkConnectedOverlayPositions]="overlayPostition"
|
||||
[cdkConnectedOverlayPositions]="overlayPosition"
|
||||
>
|
||||
<div class="box-content">
|
||||
<div class="environment-selector-dialog" [@transformPanel]="'open'" role="dialog">
|
||||
@@ -41,7 +45,7 @@
|
||||
"
|
||||
></i>
|
||||
<img class="img-us" alt="" />
|
||||
<span>{{ "us" | i18n }}</span>
|
||||
<span>{{ "usDomain" | i18n }}</span>
|
||||
</button>
|
||||
<br />
|
||||
<button
|
||||
@@ -59,7 +63,7 @@
|
||||
"
|
||||
></i>
|
||||
<img class="img-eu" alt="" />
|
||||
<span>{{ "eu" | i18n }}</span>
|
||||
<span>{{ "euDomain" | i18n }}</span>
|
||||
</button>
|
||||
<br *ngIf="euServerFlagEnabled" />
|
||||
<button
|
||||
@@ -76,8 +80,8 @@
|
||||
"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-pencil-square"
|
||||
style="padding-bottom: 1px"
|
||||
class="bwi bwi-fw bwi-md bwi-pencil-square"
|
||||
style="padding-bottom: 1px; margin-right: 5px"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span>{{ "selfHosted" | i18n }}</span>
|
||||
|
||||
@@ -6,7 +6,10 @@ import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
EnvironmentService as EnvironmentServiceAbstraction,
|
||||
Region,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
@Component({
|
||||
selector: "environment-selector",
|
||||
@@ -37,9 +40,9 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
euServerFlagEnabled: boolean;
|
||||
isOpen = false;
|
||||
showingModal = false;
|
||||
selectedEnvironment: ServerEnvironment;
|
||||
ServerEnvironmentType = ServerEnvironment;
|
||||
overlayPostition: ConnectedPosition[] = [
|
||||
selectedEnvironment: Region;
|
||||
ServerEnvironmentType = Region;
|
||||
overlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "start",
|
||||
originY: "bottom",
|
||||
@@ -50,7 +53,7 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
protected componentDestroyed$: Subject<void> = new Subject();
|
||||
|
||||
constructor(
|
||||
protected environmentService: EnvironmentService,
|
||||
protected environmentService: EnvironmentServiceAbstraction,
|
||||
protected configService: ConfigServiceAbstraction,
|
||||
protected router: Router
|
||||
) {}
|
||||
@@ -67,33 +70,28 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
this.componentDestroyed$.complete();
|
||||
}
|
||||
|
||||
async toggle(option: ServerEnvironment) {
|
||||
async toggle(option: Region) {
|
||||
this.isOpen = !this.isOpen;
|
||||
if (option === null) {
|
||||
return;
|
||||
}
|
||||
if (option === ServerEnvironment.EU) {
|
||||
await this.environmentService.setUrls({ base: "https://vault.bitwarden.eu" });
|
||||
} else if (option === ServerEnvironment.US) {
|
||||
await this.environmentService.setUrls({ base: "https://vault.bitwarden.com" });
|
||||
} else if (option === ServerEnvironment.SelfHosted) {
|
||||
|
||||
this.updateEnvironmentInfo();
|
||||
|
||||
if (option === Region.SelfHosted) {
|
||||
this.onOpenSelfHostedSettings.emit();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.environmentService.setRegion(option);
|
||||
this.updateEnvironmentInfo();
|
||||
}
|
||||
|
||||
async updateEnvironmentInfo() {
|
||||
this.selectedEnvironment = this.environmentService.selectedRegion;
|
||||
this.euServerFlagEnabled = await this.configService.getFeatureFlagBool(
|
||||
FeatureFlag.DisplayEuEnvironmentFlag
|
||||
);
|
||||
const webvaultUrl = this.environmentService.getWebVaultUrl();
|
||||
if (this.environmentService.isSelfHosted()) {
|
||||
this.selectedEnvironment = ServerEnvironment.SelfHosted;
|
||||
} else if (webvaultUrl != null && webvaultUrl.includes("bitwarden.eu")) {
|
||||
this.selectedEnvironment = ServerEnvironment.EU;
|
||||
} else {
|
||||
this.selectedEnvironment = ServerEnvironment.US;
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
@@ -101,9 +99,3 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
this.updateEnvironmentInfo();
|
||||
}
|
||||
}
|
||||
|
||||
enum ServerEnvironment {
|
||||
US = "US",
|
||||
EU = "EU",
|
||||
SelfHosted = "Self-hosted",
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export class LoginWithDeviceComponent
|
||||
protected successRoute = "vault";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
private resendTimeout = 12000;
|
||||
private authRequestKeyPair: [publicKey: ArrayBuffer, privateKey: ArrayBuffer];
|
||||
private authRequestKeyPair: [publicKey: Uint8Array, privateKey: Uint8Array];
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
|
||||
@@ -2,9 +2,9 @@ import { Directive } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
|
||||
@@ -2,9 +2,9 @@ import { Directive } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Directive } from "@angular/core";
|
||||
import { FormBuilder, FormControl } from "@angular/forms";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Verification } from "@bitwarden/common/types/verification";
|
||||
|
||||
import { ModalRef } from "../../components/modal/modal.ref";
|
||||
import { ModalConfig } from "../../services/modal.service";
|
||||
@@ -16,7 +17,12 @@ export class UserVerificationPromptComponent {
|
||||
confirmDescription = this.config.data.confirmDescription;
|
||||
confirmButtonText = this.config.data.confirmButtonText;
|
||||
modalTitle = this.config.data.modalTitle;
|
||||
secret = new FormControl();
|
||||
|
||||
formGroup = this.formBuilder.group({
|
||||
secret: this.formBuilder.control<Verification | null>(null),
|
||||
});
|
||||
|
||||
protected invalidSecret = false;
|
||||
|
||||
constructor(
|
||||
private modalRef: ModalRef,
|
||||
@@ -27,19 +33,31 @@ export class UserVerificationPromptComponent {
|
||||
private i18nService: I18nService
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
//Incorrect secret will throw an invalid password error.
|
||||
await this.userVerificationService.verifyUser(this.secret.value);
|
||||
} catch (e) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("error"),
|
||||
this.i18nService.t("invalidMasterPassword")
|
||||
);
|
||||
get secret() {
|
||||
return this.formGroup.controls.secret;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.modalRef.close(true);
|
||||
try {
|
||||
//Incorrect secret will throw an invalid password error.
|
||||
await this.userVerificationService.verifyUser(this.secret.value);
|
||||
this.invalidSecret = false;
|
||||
} catch (e) {
|
||||
this.invalidSecret = true;
|
||||
this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.close(true);
|
||||
};
|
||||
|
||||
close(success: boolean) {
|
||||
this.modalRef.close(success);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { ControlValueAccessor, FormControl } from "@angular/forms";
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { ControlValueAccessor, FormControl, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { Verification } from "@bitwarden/common/types/verification";
|
||||
|
||||
@@ -17,29 +19,65 @@ import { Verification } from "@bitwarden/common/types/verification";
|
||||
selector: "app-user-verification",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class UserVerificationComponent implements ControlValueAccessor, OnInit {
|
||||
usesKeyConnector = false;
|
||||
export class UserVerificationComponent implements ControlValueAccessor, OnInit, OnDestroy {
|
||||
private _invalidSecret = false;
|
||||
@Input()
|
||||
get invalidSecret() {
|
||||
return this._invalidSecret;
|
||||
}
|
||||
set invalidSecret(value: boolean) {
|
||||
this._invalidSecret = value;
|
||||
this.invalidSecretChange.emit(value);
|
||||
|
||||
// ISSUE: This is pretty hacky but unfortunately there is no way of knowing if the parent
|
||||
// control has been marked as touched, see: https://github.com/angular/angular/issues/10887
|
||||
// When that functionality has been added we should also look into forwarding reactive form
|
||||
// controls errors so that we don't need a separate input/output `invalidSecret`.
|
||||
if (value) {
|
||||
this.secret.markAsTouched();
|
||||
}
|
||||
this.secret.updateValueAndValidity({ emitEvent: false });
|
||||
}
|
||||
@Output() invalidSecretChange = new EventEmitter<boolean>();
|
||||
|
||||
usesKeyConnector = true;
|
||||
disableRequestOTP = false;
|
||||
sentCode = false;
|
||||
|
||||
secret = new FormControl("");
|
||||
secret = new FormControl("", [
|
||||
Validators.required,
|
||||
() => {
|
||||
if (this.invalidSecret) {
|
||||
return {
|
||||
invalidSecret: {
|
||||
message: this.usesKeyConnector
|
||||
? this.i18nService.t("incorrectCode")
|
||||
: this.i18nService.t("incorrectPassword"),
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
private onChange: (value: Verification) => void;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private userVerificationService: UserVerificationService
|
||||
private userVerificationService: UserVerificationService,
|
||||
private i18nService: I18nService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
|
||||
this.processChanges(this.secret.value);
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.secret.valueChanges.subscribe((secret: string) => this.processChanges(secret));
|
||||
this.secret.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((secret: string) => this.processChanges(secret));
|
||||
}
|
||||
|
||||
async requestOTP() {
|
||||
requestOTP = async () => {
|
||||
if (this.usesKeyConnector) {
|
||||
this.disableRequestOTP = true;
|
||||
try {
|
||||
@@ -49,7 +87,7 @@ export class UserVerificationComponent implements ControlValueAccessor, OnInit {
|
||||
this.disableRequestOTP = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
writeValue(obj: any): void {
|
||||
this.secret.setValue(obj);
|
||||
@@ -72,7 +110,14 @@ export class UserVerificationComponent implements ControlValueAccessor, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
private processChanges(secret: string) {
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
protected processChanges(secret: string) {
|
||||
this.invalidSecret = false;
|
||||
|
||||
if (this.onChange == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Directive, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Region,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@@ -25,6 +28,9 @@ export class EnvironmentComponent {
|
||||
private modalService: ModalService
|
||||
) {
|
||||
const urls = this.environmentService.getUrls();
|
||||
if (this.environmentService.selectedRegion != Region.SelfHosted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.baseUrl = urls.base || "";
|
||||
this.webVaultUrl = urls.webVault || "";
|
||||
|
||||
@@ -133,10 +133,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
||||
|
||||
// RSA Encrypt user's encKey.key with organization public key
|
||||
const userEncKey = await this.cryptoService.getEncKey();
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(
|
||||
userEncKey.key,
|
||||
publicKey.buffer
|
||||
);
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(userEncKey.key, publicKey);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.masterPasswordHash = masterPasswordHash;
|
||||
|
||||
20
libs/angular/src/directives/copy-text.directive.ts
Normal file
20
libs/angular/src/directives/copy-text.directive.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Directive, ElementRef, HostListener, Input } from "@angular/core";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@Directive({
|
||||
selector: "[appCopyText]",
|
||||
})
|
||||
export class CopyTextDirective {
|
||||
constructor(private el: ElementRef, private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
@Input("appCopyText") copyText: string;
|
||||
|
||||
@HostListener("copy") onCopy() {
|
||||
if (window == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.platformUtilsService.copyToClipboard(this.copyText, { window: window });
|
||||
}
|
||||
}
|
||||
137
libs/angular/src/directives/if-feature.directive.spec.ts
Normal file
137
libs/angular/src/directives/if-feature.directive.spec.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
import { IfFeatureDirective } from "./if-feature.directive";
|
||||
|
||||
const testBooleanFeature: FeatureFlag = "boolean-feature" as FeatureFlag;
|
||||
const testStringFeature: FeatureFlag = "string-feature" as FeatureFlag;
|
||||
const testStringFeatureValue = "test-value";
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div *appIfFeature="testBooleanFeature">
|
||||
<div data-testid="boolean-content">Hidden behind feature flag</div>
|
||||
</div>
|
||||
<div *appIfFeature="stringFeature; value: stringFeatureValue">
|
||||
<div data-testid="string-content">Hidden behind feature flag</div>
|
||||
</div>
|
||||
<div *appIfFeature="missingFlag">
|
||||
<div data-testid="missing-flag-content">
|
||||
Hidden behind missing flag. Should not be visible.
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class TestComponent {
|
||||
testBooleanFeature = testBooleanFeature;
|
||||
stringFeature = testStringFeature;
|
||||
stringFeatureValue = testStringFeatureValue;
|
||||
|
||||
missingFlag = "missing-flag" as FeatureFlag;
|
||||
}
|
||||
|
||||
describe("IfFeatureDirective", () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let content: HTMLElement;
|
||||
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
|
||||
|
||||
const mockConfigFlagValue = (flag: FeatureFlag, flagValue: any) => {
|
||||
if (typeof flagValue === "boolean") {
|
||||
mockConfigService.getFeatureFlagBool.mockImplementation((f, defaultValue = false) =>
|
||||
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
|
||||
);
|
||||
} else if (typeof flagValue === "string") {
|
||||
mockConfigService.getFeatureFlagString.mockImplementation((f, defaultValue = "") =>
|
||||
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
|
||||
);
|
||||
} else if (typeof flagValue === "number") {
|
||||
mockConfigService.getFeatureFlagNumber.mockImplementation((f, defaultValue = 0) =>
|
||||
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
|
||||
);
|
||||
}
|
||||
};
|
||||
const queryContent = (testId: string) =>
|
||||
fixture.debugElement.query(By.css(`[data-testid="${testId}"]`))?.nativeElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockConfigService = mock<ConfigServiceAbstraction>();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [IfFeatureDirective, TestComponent],
|
||||
providers: [
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{
|
||||
provide: ConfigServiceAbstraction,
|
||||
useValue: mockConfigService,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
});
|
||||
|
||||
it("renders content when the feature flag is enabled", async () => {
|
||||
mockConfigFlagValue(testBooleanFeature, true);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
content = queryContent("boolean-content");
|
||||
|
||||
expect(content).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders content when the feature flag value matches the provided value", async () => {
|
||||
mockConfigFlagValue(testStringFeature, testStringFeatureValue);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
content = queryContent("string-content");
|
||||
|
||||
expect(content).toBeDefined();
|
||||
});
|
||||
|
||||
it("hides content when the feature flag is disabled", async () => {
|
||||
mockConfigFlagValue(testBooleanFeature, false);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
content = queryContent("boolean-content");
|
||||
|
||||
expect(content).toBeUndefined();
|
||||
});
|
||||
|
||||
it("hides content when the feature flag value does not match the provided value", async () => {
|
||||
mockConfigFlagValue(testStringFeature, "wrong-value");
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
content = queryContent("string-content");
|
||||
|
||||
expect(content).toBeUndefined();
|
||||
});
|
||||
|
||||
it("hides content when the feature flag is missing", async () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
content = queryContent("missing-flag-content");
|
||||
|
||||
expect(content).toBeUndefined();
|
||||
});
|
||||
|
||||
it("hides content when the directive throws an unexpected exception", async () => {
|
||||
mockConfigService.getFeatureFlagBool.mockImplementation(() => Promise.reject("Some error"));
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
content = queryContent("boolean-content");
|
||||
|
||||
expect(content).toBeUndefined();
|
||||
});
|
||||
});
|
||||
67
libs/angular/src/directives/if-feature.directive.ts
Normal file
67
libs/angular/src/directives/if-feature.directive.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
||||
type FlagValue = boolean | number | string;
|
||||
|
||||
/**
|
||||
* Directive that conditionally renders the element when the feature flag is enabled and/or
|
||||
* matches the value specified by {@link appIfFeatureValue}.
|
||||
*
|
||||
* When a feature flag is not found in the config service, the element is hidden.
|
||||
*/
|
||||
@Directive({
|
||||
selector: "[appIfFeature]",
|
||||
})
|
||||
export class IfFeatureDirective implements OnInit {
|
||||
/**
|
||||
* The feature flag to check.
|
||||
*/
|
||||
@Input() appIfFeature: FeatureFlag;
|
||||
|
||||
/**
|
||||
* Optional value to compare against the value of the feature flag in the config service.
|
||||
* @default true
|
||||
*/
|
||||
@Input() appIfFeatureValue: FlagValue = true;
|
||||
|
||||
private hasView = false;
|
||||
|
||||
constructor(
|
||||
private templateRef: TemplateRef<any>,
|
||||
private viewContainer: ViewContainerRef,
|
||||
private configService: ConfigServiceAbstraction,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
let flagValue: FlagValue;
|
||||
|
||||
if (typeof this.appIfFeatureValue === "boolean") {
|
||||
flagValue = await this.configService.getFeatureFlagBool(this.appIfFeature);
|
||||
} else if (typeof this.appIfFeatureValue === "number") {
|
||||
flagValue = await this.configService.getFeatureFlagNumber(this.appIfFeature);
|
||||
} else if (typeof this.appIfFeatureValue === "string") {
|
||||
flagValue = await this.configService.getFeatureFlagString(this.appIfFeature);
|
||||
}
|
||||
|
||||
if (this.appIfFeatureValue === flagValue) {
|
||||
if (!this.hasView) {
|
||||
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||
this.hasView = true;
|
||||
}
|
||||
} else {
|
||||
this.viewContainer.clear();
|
||||
this.hasView = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
this.viewContainer.clear();
|
||||
this.hasView = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Directive, ElementRef, HostListener } from "@angular/core";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@Directive({
|
||||
selector: "[appSelectCopy]",
|
||||
})
|
||||
export class SelectCopyDirective {
|
||||
constructor(private el: ElementRef, private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
@HostListener("copy") onCopy() {
|
||||
if (window == null) {
|
||||
return;
|
||||
}
|
||||
let copyText = "";
|
||||
const selection = window.getSelection();
|
||||
for (let i = 0; i < selection.rangeCount; i++) {
|
||||
const range = selection.getRangeAt(i);
|
||||
const text = range.toString();
|
||||
|
||||
// The selection should only contain one line of text. In some cases however, the
|
||||
// selection contains newlines and space characters from the indentation of following
|
||||
// sibling nodes. To avoid copying passwords containing trailing newlines and spaces
|
||||
// that aren't part of the password, the selection has to be trimmed.
|
||||
let stringEndPos = text.length;
|
||||
const newLinePos = text.search(/(?:\r\n|\r|\n)/);
|
||||
if (newLinePos > -1) {
|
||||
const otherPart = text.substr(newLinePos).trim();
|
||||
if (otherPart === "") {
|
||||
stringEndPos = newLinePos;
|
||||
}
|
||||
}
|
||||
copyText += text.substring(0, stringEndPos);
|
||||
}
|
||||
this.platformUtilsService.copyToClipboard(copyText, { window: window });
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,13 @@ import { ApiActionDirective } from "./directives/api-action.directive";
|
||||
import { AutofocusDirective } from "./directives/autofocus.directive";
|
||||
import { BoxRowDirective } from "./directives/box-row.directive";
|
||||
import { CopyClickDirective } from "./directives/copy-click.directive";
|
||||
import { CopyTextDirective } from "./directives/copy-text.directive";
|
||||
import { FallbackSrcDirective } from "./directives/fallback-src.directive";
|
||||
import { IfFeatureDirective } from "./directives/if-feature.directive";
|
||||
import { InputStripSpacesDirective } from "./directives/input-strip-spaces.directive";
|
||||
import { InputVerbatimDirective } from "./directives/input-verbatim.directive";
|
||||
import { LaunchClickDirective } from "./directives/launch-click.directive";
|
||||
import { NotPremiumDirective } from "./directives/not-premium.directive";
|
||||
import { SelectCopyDirective } from "./directives/select-copy.directive";
|
||||
import { StopClickDirective } from "./directives/stop-click.directive";
|
||||
import { StopPropDirective } from "./directives/stop-prop.directive";
|
||||
import { TrueFalseValueDirective } from "./directives/true-false-value.directive";
|
||||
@@ -25,6 +26,7 @@ import { SearchPipe } from "./pipes/search.pipe";
|
||||
import { UserNamePipe } from "./pipes/user-name.pipe";
|
||||
import { UserTypePipe } from "./pipes/user-type.pipe";
|
||||
import { EllipsisPipe } from "./platform/pipes/ellipsis.pipe";
|
||||
import { FingerprintPipe } from "./platform/pipes/fingerprint.pipe";
|
||||
import { I18nPipe } from "./platform/pipes/i18n.pipe";
|
||||
import { PasswordStrengthComponent } from "./shared/components/password-strength/password-strength.component";
|
||||
import { ExportScopeCalloutComponent } from "./tools/export/components/export-scope-callout.component";
|
||||
@@ -48,6 +50,7 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
AutofocusDirective,
|
||||
BoxRowDirective,
|
||||
CalloutComponent,
|
||||
CopyTextDirective,
|
||||
CreditCardNumberPipe,
|
||||
EllipsisPipe,
|
||||
ExportScopeCalloutComponent,
|
||||
@@ -59,7 +62,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
NotPremiumDirective,
|
||||
SearchCiphersPipe,
|
||||
SearchPipe,
|
||||
SelectCopyDirective,
|
||||
StopClickDirective,
|
||||
StopPropDirective,
|
||||
TrueFalseValueDirective,
|
||||
@@ -68,6 +70,8 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
UserNamePipe,
|
||||
PasswordStrengthComponent,
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
],
|
||||
exports: [
|
||||
A11yInvalidDirective,
|
||||
@@ -77,6 +81,7 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
BitwardenToastModule,
|
||||
BoxRowDirective,
|
||||
CalloutComponent,
|
||||
CopyTextDirective,
|
||||
CreditCardNumberPipe,
|
||||
EllipsisPipe,
|
||||
ExportScopeCalloutComponent,
|
||||
@@ -88,7 +93,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
NotPremiumDirective,
|
||||
SearchCiphersPipe,
|
||||
SearchPipe,
|
||||
SelectCopyDirective,
|
||||
StopClickDirective,
|
||||
StopPropDirective,
|
||||
TrueFalseValueDirective,
|
||||
@@ -97,7 +101,17 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
UserNamePipe,
|
||||
PasswordStrengthComponent,
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
],
|
||||
providers: [
|
||||
CreditCardNumberPipe,
|
||||
DatePipe,
|
||||
I18nPipe,
|
||||
SearchPipe,
|
||||
UserNamePipe,
|
||||
UserTypePipe,
|
||||
FingerprintPipe,
|
||||
],
|
||||
providers: [CreditCardNumberPipe, DatePipe, I18nPipe, SearchPipe, UserNamePipe, UserTypePipe],
|
||||
})
|
||||
export class JslibModule {}
|
||||
|
||||
29
libs/angular/src/platform/pipes/fingerprint.pipe.ts
Normal file
29
libs/angular/src/platform/pipes/fingerprint.pipe.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Pipe } from "@angular/core";
|
||||
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@Pipe({
|
||||
name: "fingerprint",
|
||||
})
|
||||
export class FingerprintPipe {
|
||||
constructor(private cryptoService: CryptoService) {}
|
||||
|
||||
async transform(publicKey: string | Uint8Array, fingerprintMaterial: string): Promise<string> {
|
||||
try {
|
||||
if (typeof publicKey === "string") {
|
||||
publicKey = Utils.fromB64ToArray(publicKey);
|
||||
}
|
||||
|
||||
const fingerprint = await this.cryptoService.getFingerprint(fingerprintMaterial, publicKey);
|
||||
|
||||
if (fingerprint != null) {
|
||||
return fingerprint.join("-");
|
||||
}
|
||||
|
||||
return "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,4 +48,10 @@ export type SimpleDialogOptions = {
|
||||
|
||||
/** Whether or not the user can use escape or clicking the backdrop to close the dialog */
|
||||
disableClose?: boolean;
|
||||
|
||||
/**
|
||||
* Custom accept action. Runs when the user clicks the accept button and shows a loading spinner until the promise
|
||||
* is resolved.
|
||||
*/
|
||||
acceptAction?: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -18,13 +18,11 @@ import { OrganizationUserService } from "@bitwarden/common/abstractions/organiza
|
||||
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
|
||||
import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service";
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service";
|
||||
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/abstractions/userVerification/userVerification-api.service.abstraction";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
|
||||
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import {
|
||||
InternalOrganizationService,
|
||||
InternalOrganizationServiceAbstraction,
|
||||
OrganizationService as OrganizationServiceAbstraction,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
@@ -48,6 +46,8 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarde
|
||||
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service";
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
@@ -583,7 +583,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
deps: [StateServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: InternalOrganizationService,
|
||||
provide: InternalOrganizationServiceAbstraction,
|
||||
useExisting: OrganizationServiceAbstraction,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ExportScopeCalloutComponent implements OnInit {
|
||||
this.organizationId != null
|
||||
? {
|
||||
title: "exportingOrganizationVaultTitle",
|
||||
description: "exportingOrganizationVaultDescription",
|
||||
description: "exportingOrganizationVaultDesc",
|
||||
scopeIdentifier: this.organizationService.get(this.organizationId).name,
|
||||
}
|
||||
: {
|
||||
|
||||
@@ -3,9 +3,9 @@ import { UntypedFormBuilder, Validators } from "@angular/forms";
|
||||
import { merge, startWith, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { EncryptedExportType, EventType } from "@bitwarden/common/enums";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
|
||||
@@ -25,11 +25,9 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
expired = false;
|
||||
type: SendType = null;
|
||||
sends: SendView[] = [];
|
||||
filteredSends: SendView[] = [];
|
||||
searchText: string;
|
||||
selectedType: SendType;
|
||||
selectedAll: boolean;
|
||||
searchPlaceholder: string;
|
||||
filter: (cipher: SendView) => boolean;
|
||||
searchPending = false;
|
||||
hasSearched = false; // search() function called - returns true if text qualifies for search
|
||||
@@ -41,6 +39,15 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
|
||||
private searchTimeout: any;
|
||||
private destroy$ = new Subject<void>();
|
||||
private _filteredSends: SendView[];
|
||||
|
||||
get filteredSends(): SendView[] {
|
||||
return this._filteredSends;
|
||||
}
|
||||
|
||||
set filteredSends(filteredSends: SendView[]) {
|
||||
this._filteredSends = filteredSends;
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected sendService: SendService,
|
||||
|
||||
@@ -604,9 +604,11 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
protected saveCipher(cipher: Cipher) {
|
||||
const isNotClone = this.editMode && !this.cloneMode;
|
||||
const orgAdmin = this.organization?.isAdmin;
|
||||
return this.cipher.id == null
|
||||
? this.cipherService.createWithServer(cipher)
|
||||
: this.cipherService.updateWithServer(cipher);
|
||||
? this.cipherService.createWithServer(cipher, orgAdmin)
|
||||
: this.cipherService.updateWithServer(cipher, orgAdmin, isNotClone);
|
||||
}
|
||||
|
||||
protected deleteCipher() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { Validators, FormBuilder } from "@angular/forms";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -22,13 +23,18 @@ export class FolderAddEditComponent implements OnInit {
|
||||
deletePromise: Promise<any>;
|
||||
protected componentName = "";
|
||||
|
||||
formGroup = this.formBuilder.group({
|
||||
name: ["", [Validators.required]],
|
||||
});
|
||||
|
||||
constructor(
|
||||
protected folderService: FolderService,
|
||||
protected folderApiService: FolderApiServiceAbstraction,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
protected logService: LogService,
|
||||
protected dialogService: DialogServiceAbstraction,
|
||||
protected formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -36,6 +42,7 @@ export class FolderAddEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
this.folder.name = this.formGroup.controls.name.value;
|
||||
if (this.folder.name == null || this.folder.name === "") {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
@@ -97,5 +104,6 @@ export class FolderAddEditComponent implements OnInit {
|
||||
} else {
|
||||
this.title = this.i18nService.t("addFolder");
|
||||
}
|
||||
this.formGroup.controls.name.setValue(this.folder.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -13,6 +14,7 @@ export class PremiumComponent implements OnInit {
|
||||
isPremium = false;
|
||||
price = 10;
|
||||
refreshPromise: Promise<any>;
|
||||
cloudWebVaultUrl: string;
|
||||
|
||||
constructor(
|
||||
protected i18nService: I18nService,
|
||||
@@ -20,8 +22,11 @@ export class PremiumComponent implements OnInit {
|
||||
protected apiService: ApiService,
|
||||
private logService: LogService,
|
||||
protected stateService: StateService,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
) {}
|
||||
protected dialogService: DialogServiceAbstraction,
|
||||
private environmentService: EnvironmentService
|
||||
) {
|
||||
this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isPremium = await this.stateService.getCanAccessPremium();
|
||||
@@ -46,7 +51,7 @@ export class PremiumComponent implements OnInit {
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.platformUtilsService.launchUri("https://vault.bitwarden.com/#/?premium=purchase");
|
||||
this.platformUtilsService.launchUri(`${this.cloudWebVaultUrl}/#/?premium=purchase`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +63,7 @@ export class PremiumComponent implements OnInit {
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.platformUtilsService.launchUri("https://vault.bitwarden.com/#/?premium=manage");
|
||||
this.platformUtilsService.launchUri(`${this.cloudWebVaultUrl}/#/?premium=manage`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,16 +329,16 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
this.platformUtilsService.launchUri(uri.launchUri);
|
||||
}
|
||||
|
||||
async copy(value: string, typeI18nKey: string, aType: string) {
|
||||
async copy(value: string, typeI18nKey: string, aType: string): Promise<boolean> {
|
||||
if (value == null) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.passwordRepromptService.protectedFields().includes(aType) &&
|
||||
!(await this.promptPassword())
|
||||
) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const copyOptions = this.win != null ? { window: this.win } : null;
|
||||
@@ -356,6 +356,8 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
} else if (aType === "H_Field") {
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setTextDataOnDrag(event: DragEvent, data: string) {
|
||||
|
||||
3
libs/auth/README.md
Normal file
3
libs/auth/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Auth
|
||||
|
||||
This lib represents the public API of the Auth team at Bitwarden. Modules are imported using `@bitwarden/auth`.
|
||||
16
libs/auth/jest.config.js
Normal file
16
libs/auth/jest.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
|
||||
const { compilerOptions } = require("../shared/tsconfig.libs");
|
||||
|
||||
const sharedConfig = require("../../libs/shared/jest.config.angular");
|
||||
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
displayName: "libs/auth tests",
|
||||
preset: "jest-preset-angular",
|
||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||
prefix: "<rootDir>/",
|
||||
}),
|
||||
};
|
||||
20
libs/auth/package.json
Normal file
20
libs/auth/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@bitwarden/auth",
|
||||
"version": "0.0.0",
|
||||
"description": "Common code used across Bitwarden JavaScript projects.",
|
||||
"keywords": [
|
||||
"bitwarden"
|
||||
],
|
||||
"author": "Bitwarden Inc.",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bitwarden/clients"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && tsc",
|
||||
"build:watch": "npm run clean && tsc -watch"
|
||||
}
|
||||
}
|
||||
0
libs/auth/src/index.ts
Normal file
0
libs/auth/src/index.ts
Normal file
28
libs/auth/test.setup.ts
Normal file
28
libs/auth/test.setup.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { webcrypto } from "crypto";
|
||||
import "jest-preset-angular/setup-jest";
|
||||
|
||||
Object.defineProperty(window, "CSS", { value: null });
|
||||
Object.defineProperty(window, "getComputedStyle", {
|
||||
value: () => {
|
||||
return {
|
||||
display: "none",
|
||||
appearance: ["-webkit-appearance"],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(document, "doctype", {
|
||||
value: "<!DOCTYPE html>",
|
||||
});
|
||||
Object.defineProperty(document.body.style, "transform", {
|
||||
value: () => {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: webcrypto,
|
||||
});
|
||||
5
libs/auth/tsconfig.json
Normal file
5
libs/auth/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../shared/tsconfig.libs",
|
||||
"include": ["src", "spec"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
4
libs/auth/tsconfig.spec.json
Normal file
4
libs/auth/tsconfig.spec.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"files": ["./test.setup.ts"]
|
||||
}
|
||||
15
libs/common/custom-matchers.d.ts
vendored
Normal file
15
libs/common/custom-matchers.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { CustomMatchers } from "./test.setup";
|
||||
|
||||
// This declares the types for our custom matchers so that they're recognised by Typescript
|
||||
// This file must also be included in the TS compilation (via the tsconfig.json "include" property) to be recognised by
|
||||
// vscode
|
||||
|
||||
/* eslint-disable */
|
||||
declare global {
|
||||
namespace jest {
|
||||
interface Expect extends CustomMatchers {}
|
||||
interface Matchers<R> extends CustomMatchers<R> {}
|
||||
interface InverseAsymmetricMatchers extends CustomMatchers {}
|
||||
}
|
||||
}
|
||||
/* eslint-enable */
|
||||
@@ -5,14 +5,11 @@
|
||||
* (and optionally, the expected value) and then call toEqual() on the resulting Uint8Arrays.
|
||||
*/
|
||||
export const toEqualBuffer: jest.CustomMatcher = function (
|
||||
received: ArrayBuffer,
|
||||
expected: Uint8Array | ArrayBuffer
|
||||
received: ArrayBuffer | Uint8Array,
|
||||
expected: ArrayBuffer | Uint8Array
|
||||
) {
|
||||
received = new Uint8Array(received);
|
||||
|
||||
if (expected instanceof ArrayBuffer) {
|
||||
expected = new Uint8Array(expected);
|
||||
}
|
||||
expected = new Uint8Array(expected);
|
||||
|
||||
if (this.equals(received, expected)) {
|
||||
return {
|
||||
|
||||
@@ -243,6 +243,9 @@ export abstract class ApiService {
|
||||
putRestoreManyCiphers: (
|
||||
request: CipherBulkRestoreRequest
|
||||
) => Promise<ListResponse<CipherResponse>>;
|
||||
putRestoreManyCiphersAdmin: (
|
||||
request: CipherBulkRestoreRequest
|
||||
) => Promise<ListResponse<CipherResponse>>;
|
||||
|
||||
/**
|
||||
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.
|
||||
|
||||
@@ -201,6 +201,17 @@ export abstract class OrganizationUserService {
|
||||
request: OrganizationUserResetPasswordRequest
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Enable Secrets Manager for many users
|
||||
* @param organizationId - Identifier for the organization the user belongs to
|
||||
* @param ids - List of organization user identifiers to enable
|
||||
* @return List of user ids, including both those that were successfully enabled and those that had an error
|
||||
*/
|
||||
abstract putOrganizationUserBulkEnableSecretsManager(
|
||||
organizationId: string,
|
||||
ids: string[]
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Delete an organization user
|
||||
* @param organizationId - Identifier for the organization the user belongs to
|
||||
|
||||
@@ -14,6 +14,7 @@ export class OrganizationUserResponse extends BaseResponse {
|
||||
accessSecretsManager: boolean;
|
||||
permissions: PermissionsApi;
|
||||
resetPasswordEnrolled: boolean;
|
||||
hasMasterPassword: boolean;
|
||||
collections: SelectionReadOnlyResponse[] = [];
|
||||
groups: string[] = [];
|
||||
|
||||
@@ -28,6 +29,7 @@ export class OrganizationUserResponse extends BaseResponse {
|
||||
this.accessAll = this.getResponseProperty("AccessAll");
|
||||
this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager");
|
||||
this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled");
|
||||
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");
|
||||
|
||||
const collections = this.getResponseProperty("Collections");
|
||||
if (collections != null) {
|
||||
|
||||
@@ -3,9 +3,11 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio
|
||||
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
||||
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
|
||||
import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response";
|
||||
import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request";
|
||||
import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request";
|
||||
import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request";
|
||||
import { PaymentRequest } from "../../../billing/models/request/payment.request";
|
||||
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
|
||||
import { BillingResponse } from "../../../billing/models/response/billing.response";
|
||||
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
||||
import { PaymentResponse } from "../../../billing/models/response/payment.response";
|
||||
@@ -16,7 +18,6 @@ import { StorageRequest } from "../../../models/request/storage.request";
|
||||
import { VerifyBankRequest } from "../../../models/request/verify-bank.request";
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { OrganizationApiKeyType } from "../../enums";
|
||||
import { OrganizationEnrollSecretsManagerRequest } from "../../models/request/organization/organization-enroll-secrets-manager.request";
|
||||
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
|
||||
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
|
||||
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
|
||||
@@ -25,6 +26,7 @@ import { OrganizationApiKeyInformationResponse } from "../../models/response/org
|
||||
import { OrganizationAutoEnrollStatusResponse } from "../../models/response/organization-auto-enroll-status.response";
|
||||
import { OrganizationKeysResponse } from "../../models/response/organization-keys.response";
|
||||
import { OrganizationResponse } from "../../models/response/organization.response";
|
||||
import { ProfileOrganizationResponse } from "../../models/response/profile-organization.response";
|
||||
|
||||
export class OrganizationApiServiceAbstraction {
|
||||
get: (id: string) => Promise<OrganizationResponse>;
|
||||
@@ -37,7 +39,14 @@ export class OrganizationApiServiceAbstraction {
|
||||
save: (id: string, request: OrganizationUpdateRequest) => Promise<OrganizationResponse>;
|
||||
updatePayment: (id: string, request: PaymentRequest) => Promise<void>;
|
||||
upgrade: (id: string, request: OrganizationUpgradeRequest) => Promise<PaymentResponse>;
|
||||
updateSubscription: (id: string, request: OrganizationSubscriptionUpdateRequest) => Promise<void>;
|
||||
updatePasswordManagerSeats: (
|
||||
id: string,
|
||||
request: OrganizationSubscriptionUpdateRequest
|
||||
) => Promise<void>;
|
||||
updateSecretsManagerSubscription: (
|
||||
id: string,
|
||||
request: OrganizationSmSubscriptionUpdateRequest
|
||||
) => Promise<void>;
|
||||
updateSeats: (id: string, request: SeatRequest) => Promise<PaymentResponse>;
|
||||
updateStorage: (id: string, request: StorageRequest) => Promise<PaymentResponse>;
|
||||
verifyBank: (id: string, request: VerifyBankRequest) => Promise<void>;
|
||||
@@ -60,8 +69,8 @@ export class OrganizationApiServiceAbstraction {
|
||||
getSso: (id: string) => Promise<OrganizationSsoResponse>;
|
||||
updateSso: (id: string, request: OrganizationSsoRequest) => Promise<OrganizationSsoResponse>;
|
||||
selfHostedSyncLicense: (id: string) => Promise<void>;
|
||||
updateEnrollSecretsManager: (
|
||||
subscribeToSecretsManager: (
|
||||
id: string,
|
||||
request: OrganizationEnrollSecretsManagerRequest
|
||||
) => Promise<void>;
|
||||
request: SecretsManagerSubscribeRequest
|
||||
) => Promise<ProfileOrganizationResponse>;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ export function canAccessSettingsTab(org: Organization): boolean {
|
||||
org.canManagePolicies ||
|
||||
org.canManageSso ||
|
||||
org.canManageScim ||
|
||||
org.canAccessImportExport
|
||||
org.canAccessImportExport ||
|
||||
org.canManageDeviceApprovals
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +57,12 @@ export function canAccessAdmin(i18nService: I18nService) {
|
||||
);
|
||||
}
|
||||
|
||||
export function canAccessImportExport(i18nService: I18nService) {
|
||||
return map<Organization[], Organization[]>((orgs) =>
|
||||
orgs.filter((org) => org.canAccessImportExport).sort(Utils.getSortFunction(i18nService, "name"))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if a user is a member of an organization (rather than only being a ProviderUser)
|
||||
* @deprecated Use organizationService.memberOrganizations$ instead
|
||||
@@ -85,6 +92,7 @@ export abstract class OrganizationService {
|
||||
hasOrganizations: () => boolean;
|
||||
}
|
||||
|
||||
export abstract class InternalOrganizationService extends OrganizationService {
|
||||
export abstract class InternalOrganizationServiceAbstraction extends OrganizationService {
|
||||
replace: (organizations: { [id: string]: OrganizationData }) => Promise<void>;
|
||||
upsert: (OrganizationData: OrganizationData | OrganizationData[]) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export class OrganizationData {
|
||||
useCustomPermissions: boolean;
|
||||
useResetPassword: boolean;
|
||||
useSecretsManager: boolean;
|
||||
usePasswordManager: boolean;
|
||||
useActivateAutofillPolicy: boolean;
|
||||
selfHost: boolean;
|
||||
usersGetPremium: boolean;
|
||||
@@ -74,6 +75,7 @@ export class OrganizationData {
|
||||
this.useCustomPermissions = response.useCustomPermissions;
|
||||
this.useResetPassword = response.useResetPassword;
|
||||
this.useSecretsManager = response.useSecretsManager;
|
||||
this.usePasswordManager = response.usePasswordManager;
|
||||
this.useActivateAutofillPolicy = response.useActivateAutofillPolicy;
|
||||
this.selfHost = response.selfHost;
|
||||
this.usersGetPremium = response.usersGetPremium;
|
||||
|
||||
@@ -31,6 +31,7 @@ export class Organization {
|
||||
useCustomPermissions: boolean;
|
||||
useResetPassword: boolean;
|
||||
useSecretsManager: boolean;
|
||||
usePasswordManager: boolean;
|
||||
useActivateAutofillPolicy: boolean;
|
||||
selfHost: boolean;
|
||||
usersGetPremium: boolean;
|
||||
@@ -87,6 +88,7 @@ export class Organization {
|
||||
this.useCustomPermissions = obj.useCustomPermissions;
|
||||
this.useResetPassword = obj.useResetPassword;
|
||||
this.useSecretsManager = obj.useSecretsManager;
|
||||
this.usePasswordManager = obj.usePasswordManager;
|
||||
this.useActivateAutofillPolicy = obj.useActivateAutofillPolicy;
|
||||
this.selfHost = obj.selfHost;
|
||||
this.usersGetPremium = obj.usersGetPremium;
|
||||
@@ -215,6 +217,10 @@ export class Organization {
|
||||
return this.isAdmin || this.permissions.manageResetPassword;
|
||||
}
|
||||
|
||||
get canManageDeviceApprovals() {
|
||||
return (this.isAdmin || this.permissions.manageResetPassword) && this.useSso;
|
||||
}
|
||||
|
||||
get isExemptFromPolicies() {
|
||||
return this.canManagePolicies;
|
||||
}
|
||||
|
||||
@@ -23,4 +23,8 @@ export class OrganizationCreateRequest {
|
||||
billingAddressState: string;
|
||||
billingAddressPostalCode: string;
|
||||
billingAddressCountry: string;
|
||||
|
||||
useSecretsManager: boolean;
|
||||
additionalSmSeats: number;
|
||||
additionalServiceAccounts: number;
|
||||
}
|
||||
|
||||
@@ -11,4 +11,8 @@ export class OrganizationUpgradeRequest {
|
||||
billingAddressCountry: string;
|
||||
billingAddressPostalCode: string;
|
||||
keys: OrganizationKeysRequest;
|
||||
|
||||
useSecretsManager: boolean;
|
||||
additionalSmSeats: number;
|
||||
additionalServiceAccounts: number;
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export class OrganizationEnrollSecretsManagerRequest {
|
||||
enabled: boolean;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export class OrganizationResponse extends BaseResponse {
|
||||
businessTaxNumber: string;
|
||||
billingEmail: string;
|
||||
plan: PlanResponse;
|
||||
secretsManagerPlan: PlanResponse;
|
||||
planType: PlanType;
|
||||
seats: number;
|
||||
maxAutoscaleSeats: number;
|
||||
@@ -27,6 +28,11 @@ export class OrganizationResponse extends BaseResponse {
|
||||
useResetPassword: boolean;
|
||||
useSecretsManager: boolean;
|
||||
hasPublicAndPrivateKeys: boolean;
|
||||
usePasswordManager: boolean;
|
||||
smSeats?: number;
|
||||
smServiceAccounts?: number;
|
||||
maxAutoscaleSmSeats?: number;
|
||||
maxAutoscaleSmServiceAccounts?: number;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -39,8 +45,14 @@ export class OrganizationResponse extends BaseResponse {
|
||||
this.businessCountry = this.getResponseProperty("BusinessCountry");
|
||||
this.businessTaxNumber = this.getResponseProperty("BusinessTaxNumber");
|
||||
this.billingEmail = this.getResponseProperty("BillingEmail");
|
||||
|
||||
const plan = this.getResponseProperty("Plan");
|
||||
this.plan = plan == null ? null : new PlanResponse(plan);
|
||||
|
||||
const secretsManagerPlan = this.getResponseProperty("SecretsManagerPlan");
|
||||
this.secretsManagerPlan =
|
||||
secretsManagerPlan == null ? null : new PlanResponse(secretsManagerPlan);
|
||||
|
||||
this.planType = this.getResponseProperty("PlanType");
|
||||
this.seats = this.getResponseProperty("Seats");
|
||||
this.maxAutoscaleSeats = this.getResponseProperty("MaxAutoscaleSeats");
|
||||
@@ -55,5 +67,10 @@ export class OrganizationResponse extends BaseResponse {
|
||||
this.useResetPassword = this.getResponseProperty("UseResetPassword");
|
||||
this.useSecretsManager = this.getResponseProperty("UseSecretsManager");
|
||||
this.hasPublicAndPrivateKeys = this.getResponseProperty("HasPublicAndPrivateKeys");
|
||||
this.usePasswordManager = this.getResponseProperty("UsePasswordManager");
|
||||
this.smSeats = this.getResponseProperty("SmSeats");
|
||||
this.smServiceAccounts = this.getResponseProperty("SmServiceAccounts");
|
||||
this.maxAutoscaleSmSeats = this.getResponseProperty("MaxAutoscaleSmSeats");
|
||||
this.maxAutoscaleSmServiceAccounts = this.getResponseProperty("MaxAutoscaleSmServiceAccounts");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
||||
useCustomPermissions: boolean;
|
||||
useResetPassword: boolean;
|
||||
useSecretsManager: boolean;
|
||||
usePasswordManager: boolean;
|
||||
useActivateAutofillPolicy: boolean;
|
||||
selfHost: boolean;
|
||||
usersGetPremium: boolean;
|
||||
@@ -65,6 +66,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
||||
this.useCustomPermissions = this.getResponseProperty("UseCustomPermissions") ?? false;
|
||||
this.useResetPassword = this.getResponseProperty("UseResetPassword");
|
||||
this.useSecretsManager = this.getResponseProperty("UseSecretsManager");
|
||||
this.usePasswordManager = this.getResponseProperty("UsePasswordManager");
|
||||
this.useActivateAutofillPolicy = this.getResponseProperty("UseActivateAutofillPolicy");
|
||||
this.selfHost = this.getResponseProperty("SelfHost");
|
||||
this.usersGetPremium = this.getResponseProperty("UsersGetPremium");
|
||||
|
||||
@@ -4,9 +4,11 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio
|
||||
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
||||
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
|
||||
import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response";
|
||||
import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request";
|
||||
import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request";
|
||||
import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request";
|
||||
import { PaymentRequest } from "../../../billing/models/request/payment.request";
|
||||
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
|
||||
import { BillingResponse } from "../../../billing/models/response/billing.response";
|
||||
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
||||
import { PaymentResponse } from "../../../billing/models/response/payment.response";
|
||||
@@ -19,7 +21,6 @@ import { ListResponse } from "../../../models/response/list.response";
|
||||
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
|
||||
import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationApiKeyType } from "../../enums";
|
||||
import { OrganizationEnrollSecretsManagerRequest } from "../../models/request/organization/organization-enroll-secrets-manager.request";
|
||||
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
|
||||
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
|
||||
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
|
||||
@@ -28,6 +29,7 @@ import { OrganizationApiKeyInformationResponse } from "../../models/response/org
|
||||
import { OrganizationAutoEnrollStatusResponse } from "../../models/response/organization-auto-enroll-status.response";
|
||||
import { OrganizationKeysResponse } from "../../models/response/organization-keys.response";
|
||||
import { OrganizationResponse } from "../../models/response/organization.response";
|
||||
import { ProfileOrganizationResponse } from "../../models/response/profile-organization.response";
|
||||
|
||||
export class OrganizationApiService implements OrganizationApiServiceAbstraction {
|
||||
constructor(private apiService: ApiService, private syncService: SyncService) {}
|
||||
@@ -120,7 +122,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
return new PaymentResponse(r);
|
||||
}
|
||||
|
||||
async updateSubscription(
|
||||
async updatePasswordManagerSeats(
|
||||
id: string,
|
||||
request: OrganizationSubscriptionUpdateRequest
|
||||
): Promise<void> {
|
||||
@@ -133,6 +135,19 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
);
|
||||
}
|
||||
|
||||
async updateSecretsManagerSubscription(
|
||||
id: string,
|
||||
request: OrganizationSmSubscriptionUpdateRequest
|
||||
): Promise<void> {
|
||||
return this.apiService.send(
|
||||
"POST",
|
||||
"/organizations/" + id + "/sm-subscription",
|
||||
request,
|
||||
true,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
async updateSeats(id: string, request: SeatRequest): Promise<PaymentResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"POST",
|
||||
@@ -294,13 +309,17 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
);
|
||||
}
|
||||
|
||||
async updateEnrollSecretsManager(id: string, request: OrganizationEnrollSecretsManagerRequest) {
|
||||
await this.apiService.send(
|
||||
async subscribeToSecretsManager(
|
||||
id: string,
|
||||
request: SecretsManagerSubscribeRequest
|
||||
): Promise<ProfileOrganizationResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"POST",
|
||||
"/organizations/" + id + "/enroll-secrets-manager",
|
||||
"/organizations/" + id + "/subscribe-secrets-manager",
|
||||
request,
|
||||
true,
|
||||
true
|
||||
);
|
||||
return new ProfileOrganizationResponse(r);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BehaviorSubject, concatMap, map, Observable } from "rxjs";
|
||||
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import {
|
||||
InternalOrganizationService as InternalOrganizationServiceAbstraction,
|
||||
InternalOrganizationServiceAbstraction,
|
||||
isMember,
|
||||
} from "../../abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationData } from "../../models/data/organization.data";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VerifyOTPRequest } from "../../auth/models/request/verify-otp.request";
|
||||
import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
|
||||
|
||||
export abstract class UserVerificationApiServiceAbstraction {
|
||||
postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise<void>;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SecretVerificationRequest } from "../../auth/models/request/secret-verification.request";
|
||||
import { Verification } from "../../types/verification";
|
||||
import { Verification } from "../../../types/verification";
|
||||
import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
|
||||
|
||||
export abstract class UserVerificationService {
|
||||
buildRequest: <T extends SecretVerificationRequest>(
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { UserVerificationService } from "../../abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { Verification } from "../../types/verification";
|
||||
import { AccountApiService } from "../abstractions/account-api.service";
|
||||
import { InternalAccountService } from "../abstractions/account.service";
|
||||
import { UserVerificationService } from "../abstractions/user-verification/user-verification.service.abstraction";
|
||||
|
||||
export class AccountApiServiceImplementation implements AccountApiService {
|
||||
constructor(
|
||||
|
||||
@@ -302,15 +302,18 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
(
|
||||
await this.cryptoService.getKey()
|
||||
).encKey,
|
||||
pubKey.buffer
|
||||
);
|
||||
const encryptedMasterPassword = await this.cryptoService.rsaEncrypt(
|
||||
Utils.fromUtf8ToArray(await this.stateService.getKeyHash()),
|
||||
pubKey.buffer
|
||||
pubKey
|
||||
);
|
||||
let encryptedMasterPassword = null;
|
||||
if ((await this.stateService.getKeyHash()) != null) {
|
||||
encryptedMasterPassword = await this.cryptoService.rsaEncrypt(
|
||||
Utils.fromUtf8ToArray(await this.stateService.getKeyHash()),
|
||||
pubKey
|
||||
);
|
||||
}
|
||||
const request = new PasswordlessAuthRequest(
|
||||
encryptedKey.encryptedString,
|
||||
encryptedMasterPassword.encryptedString,
|
||||
encryptedMasterPassword?.encryptedString,
|
||||
await this.appIdService.getAppId(),
|
||||
requestApproved
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { UserVerificationApiServiceAbstraction } from "../../../abstractions/userVerification/userVerification-api.service.abstraction";
|
||||
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
|
||||
|
||||
export class UserVerificationApiService implements UserVerificationApiServiceAbstraction {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { UserVerificationApiServiceAbstraction } from "../../../abstractions/userVerification/userVerification-api.service.abstraction";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../../abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { Verification } from "../../../types/verification";
|
||||
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "../../enums/verification-type";
|
||||
import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
|
||||
import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum BitwardenProductType {
|
||||
PasswordManager = 0,
|
||||
SecretsManager = 1,
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export * from "./payment-method-type.enum";
|
||||
export * from "./plan-sponsorship-type.enum";
|
||||
export * from "./plan-type.enum";
|
||||
export * from "./transaction-type.enum";
|
||||
export * from "./bitwarden-product-type.enum";
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export class OrganizationSmSubscriptionUpdateRequest {
|
||||
/**
|
||||
* The number of seats to add or remove from the subscription.
|
||||
*/
|
||||
seatAdjustment: number;
|
||||
|
||||
/**
|
||||
* The maximum number of seats that can be auto-scaled for the subscription.
|
||||
*/
|
||||
maxAutoscaleSeats?: number;
|
||||
|
||||
/**
|
||||
* The number of additional service accounts to add or remove from the subscription.
|
||||
*/
|
||||
serviceAccountAdjustment: number;
|
||||
|
||||
/**
|
||||
* The maximum number of additional service accounts that can be auto-scaled for the subscription.
|
||||
*/
|
||||
maxAutoscaleServiceAccounts?: number;
|
||||
}
|
||||
@@ -1,3 +1,23 @@
|
||||
export class OrganizationSubscriptionUpdateRequest {
|
||||
constructor(public seatAdjustment: number, public maxAutoscaleSeats?: number) {}
|
||||
/**
|
||||
* The number of seats to add or remove from the subscription.
|
||||
* Applies to both PM and SM request types.
|
||||
*/
|
||||
seatAdjustment: number;
|
||||
|
||||
/**
|
||||
* The maximum number of seats that can be auto-scaled for the subscription.
|
||||
* Applies to both PM and SM request types.
|
||||
*/
|
||||
maxAutoscaleSeats?: number;
|
||||
|
||||
/**
|
||||
* Build a subscription update request for the Password Manager product type.
|
||||
* @param seatAdjustment - The number of seats to add or remove from the subscription.
|
||||
* @param maxAutoscaleSeats - The maximum number of seats that can be auto-scaled for the subscription.
|
||||
*/
|
||||
constructor(seatAdjustment: number, maxAutoscaleSeats?: number) {
|
||||
this.seatAdjustment = seatAdjustment;
|
||||
this.maxAutoscaleSeats = maxAutoscaleSeats;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export class SecretsManagerSubscribeRequest {
|
||||
additionalSmSeats: number;
|
||||
additionalServiceAccounts: number;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
|
||||
upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse;
|
||||
expiration: string;
|
||||
expirationWithoutGracePeriod: string;
|
||||
secretsManagerBeta: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -26,5 +27,6 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
|
||||
: new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice);
|
||||
this.expiration = this.getResponseProperty("Expiration");
|
||||
this.expirationWithoutGracePeriod = this.getResponseProperty("ExpirationWithoutGracePeriod");
|
||||
this.secretsManagerBeta = this.getResponseProperty("SecretsManagerBeta");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ProductType } from "../../../enums";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { PlanType } from "../../enums";
|
||||
import { BitwardenProductType, PlanType } from "../../enums";
|
||||
|
||||
export class PlanResponse extends BaseResponse {
|
||||
type: PlanType;
|
||||
product: ProductType;
|
||||
bitwardenProduct: BitwardenProductType;
|
||||
name: string;
|
||||
isAnnual: boolean;
|
||||
nameLocalizationKey: string;
|
||||
@@ -48,6 +49,15 @@ export class PlanResponse extends BaseResponse {
|
||||
additionalStoragePricePerGb: number;
|
||||
premiumAccessOptionPrice: number;
|
||||
|
||||
// SM only
|
||||
additionalPricePerServiceAccount: number;
|
||||
baseServiceAccount: number;
|
||||
maxServiceAccount: number;
|
||||
hasAdditionalServiceAccountOption: boolean;
|
||||
maxProjects: number;
|
||||
maxAdditionalServiceAccounts: number;
|
||||
stripeServiceAccountPlanId: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.type = this.getResponseProperty("Type");
|
||||
@@ -90,5 +100,18 @@ export class PlanResponse extends BaseResponse {
|
||||
this.seatPrice = this.getResponseProperty("SeatPrice");
|
||||
this.additionalStoragePricePerGb = this.getResponseProperty("AdditionalStoragePricePerGb");
|
||||
this.premiumAccessOptionPrice = this.getResponseProperty("PremiumAccessOptionPrice");
|
||||
|
||||
this.bitwardenProduct = this.getResponseProperty("BitwardenProduct");
|
||||
this.additionalPricePerServiceAccount = this.getResponseProperty(
|
||||
"AdditionalPricePerServiceAccount"
|
||||
);
|
||||
this.baseServiceAccount = this.getResponseProperty("BaseServiceAccount");
|
||||
this.maxServiceAccount = this.getResponseProperty("MaxServiceAccount");
|
||||
this.hasAdditionalServiceAccountOption = this.getResponseProperty(
|
||||
"HasAdditionalServiceAccountOption"
|
||||
);
|
||||
this.maxProjects = this.getResponseProperty("MaxProjects");
|
||||
this.maxAdditionalServiceAccounts = this.getResponseProperty("MaxAdditionalServiceAccounts");
|
||||
this.stripeServiceAccountPlanId = this.getResponseProperty("StripeServiceAccountPlanId");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { BitwardenProductType } from "../../enums";
|
||||
|
||||
export class SubscriptionResponse extends BaseResponse {
|
||||
storageName: string;
|
||||
@@ -62,6 +63,8 @@ export class BillingSubscriptionItemResponse extends BaseResponse {
|
||||
quantity: number;
|
||||
interval: string;
|
||||
sponsoredSubscriptionItem: boolean;
|
||||
addonSubscriptionItem: boolean;
|
||||
bitwardenProduct: BitwardenProductType;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -70,6 +73,8 @@ export class BillingSubscriptionItemResponse extends BaseResponse {
|
||||
this.quantity = this.getResponseProperty("Quantity");
|
||||
this.interval = this.getResponseProperty("Interval");
|
||||
this.sponsoredSubscriptionItem = this.getResponseProperty("SponsoredSubscriptionItem");
|
||||
this.addonSubscriptionItem = this.getResponseProperty("AddonSubscriptionItem");
|
||||
this.bitwardenProduct = this.getResponseProperty("BitwardenProduct");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,4 +3,5 @@ export enum FeatureFlag {
|
||||
DisplayLowKdfIterationWarningFlag = "display-kdf-iteration-warning",
|
||||
Fido2VaultCredentials = "fido2-vault-credentials",
|
||||
TrustedDeviceEncryption = "trusted-device-encryption",
|
||||
SecretsManagerBilling = "sm-ga-billing",
|
||||
}
|
||||
|
||||
@@ -10,4 +10,5 @@ export abstract class ConfigServiceAbstraction {
|
||||
getFeatureFlagBool: (key: FeatureFlag, defaultValue?: boolean) => Promise<boolean>;
|
||||
getFeatureFlagString: (key: FeatureFlag, defaultValue?: string) => Promise<string>;
|
||||
getFeatureFlagNumber: (key: FeatureFlag, defaultValue?: number) => Promise<number>;
|
||||
getCloudRegion: (defaultValue?: string) => Promise<string>;
|
||||
}
|
||||
|
||||
@@ -4,67 +4,67 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class CryptoFunctionService {
|
||||
pbkdf2: (
|
||||
password: string | ArrayBuffer,
|
||||
salt: string | ArrayBuffer,
|
||||
password: string | Uint8Array,
|
||||
salt: string | Uint8Array,
|
||||
algorithm: "sha256" | "sha512",
|
||||
iterations: number
|
||||
) => Promise<ArrayBuffer>;
|
||||
) => Promise<Uint8Array>;
|
||||
argon2: (
|
||||
password: string | ArrayBuffer,
|
||||
salt: string | ArrayBuffer,
|
||||
password: string | Uint8Array,
|
||||
salt: string | Uint8Array,
|
||||
iterations: number,
|
||||
memory: number,
|
||||
parallelism: number
|
||||
) => Promise<ArrayBuffer>;
|
||||
) => Promise<Uint8Array>;
|
||||
hkdf: (
|
||||
ikm: ArrayBuffer,
|
||||
salt: string | ArrayBuffer,
|
||||
info: string | ArrayBuffer,
|
||||
ikm: Uint8Array,
|
||||
salt: string | Uint8Array,
|
||||
info: string | Uint8Array,
|
||||
outputByteSize: number,
|
||||
algorithm: "sha256" | "sha512"
|
||||
) => Promise<ArrayBuffer>;
|
||||
) => Promise<Uint8Array>;
|
||||
hkdfExpand: (
|
||||
prk: ArrayBuffer,
|
||||
info: string | ArrayBuffer,
|
||||
prk: Uint8Array,
|
||||
info: string | Uint8Array,
|
||||
outputByteSize: number,
|
||||
algorithm: "sha256" | "sha512"
|
||||
) => Promise<ArrayBuffer>;
|
||||
) => Promise<Uint8Array>;
|
||||
hash: (
|
||||
value: string | ArrayBuffer,
|
||||
value: string | Uint8Array,
|
||||
algorithm: "sha1" | "sha256" | "sha512" | "md5"
|
||||
) => Promise<ArrayBuffer>;
|
||||
) => Promise<Uint8Array>;
|
||||
hmac: (
|
||||
value: ArrayBuffer,
|
||||
key: ArrayBuffer,
|
||||
value: Uint8Array,
|
||||
key: Uint8Array,
|
||||
algorithm: "sha1" | "sha256" | "sha512"
|
||||
) => Promise<ArrayBuffer>;
|
||||
compare: (a: ArrayBuffer, b: ArrayBuffer) => Promise<boolean>;
|
||||
) => Promise<Uint8Array>;
|
||||
compare: (a: Uint8Array, b: Uint8Array) => Promise<boolean>;
|
||||
hmacFast: (
|
||||
value: ArrayBuffer | string,
|
||||
key: ArrayBuffer | string,
|
||||
value: Uint8Array | string,
|
||||
key: Uint8Array | string,
|
||||
algorithm: "sha1" | "sha256" | "sha512"
|
||||
) => Promise<ArrayBuffer | string>;
|
||||
compareFast: (a: ArrayBuffer | string, b: ArrayBuffer | string) => Promise<boolean>;
|
||||
aesEncrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise<ArrayBuffer>;
|
||||
) => Promise<Uint8Array | string>;
|
||||
compareFast: (a: Uint8Array | string, b: Uint8Array | string) => Promise<boolean>;
|
||||
aesEncrypt: (data: Uint8Array, iv: Uint8Array, key: Uint8Array) => Promise<Uint8Array>;
|
||||
aesDecryptFastParameters: (
|
||||
data: string,
|
||||
iv: string,
|
||||
mac: string,
|
||||
key: SymmetricCryptoKey
|
||||
) => DecryptParameters<ArrayBuffer | string>;
|
||||
aesDecryptFast: (parameters: DecryptParameters<ArrayBuffer | string>) => Promise<string>;
|
||||
aesDecrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise<ArrayBuffer>;
|
||||
) => DecryptParameters<Uint8Array | string>;
|
||||
aesDecryptFast: (parameters: DecryptParameters<Uint8Array | string>) => Promise<string>;
|
||||
aesDecrypt: (data: Uint8Array, iv: Uint8Array, key: Uint8Array) => Promise<Uint8Array>;
|
||||
rsaEncrypt: (
|
||||
data: ArrayBuffer,
|
||||
publicKey: ArrayBuffer,
|
||||
data: Uint8Array,
|
||||
publicKey: Uint8Array,
|
||||
algorithm: "sha1" | "sha256"
|
||||
) => Promise<ArrayBuffer>;
|
||||
) => Promise<Uint8Array>;
|
||||
rsaDecrypt: (
|
||||
data: ArrayBuffer,
|
||||
privateKey: ArrayBuffer,
|
||||
data: Uint8Array,
|
||||
privateKey: Uint8Array,
|
||||
algorithm: "sha1" | "sha256"
|
||||
) => Promise<ArrayBuffer>;
|
||||
rsaExtractPublicKey: (privateKey: ArrayBuffer) => Promise<ArrayBuffer>;
|
||||
rsaGenerateKeyPair: (length: 1024 | 2048 | 4096) => Promise<[ArrayBuffer, ArrayBuffer]>;
|
||||
) => Promise<Uint8Array>;
|
||||
rsaExtractPublicKey: (privateKey: Uint8Array) => Promise<Uint8Array>;
|
||||
rsaGenerateKeyPair: (length: 1024 | 2048 | 4096) => Promise<[Uint8Array, Uint8Array]>;
|
||||
randomBytes: (length: number) => Promise<CsprngArray>;
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ export abstract class CryptoService {
|
||||
getKeyHash: () => Promise<string>;
|
||||
compareAndUpdateKeyHash: (masterPassword: string, key: SymmetricCryptoKey) => Promise<boolean>;
|
||||
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
|
||||
getPublicKey: () => Promise<ArrayBuffer>;
|
||||
getPrivateKey: () => Promise<ArrayBuffer>;
|
||||
getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise<string[]>;
|
||||
getPublicKey: () => Promise<Uint8Array>;
|
||||
getPrivateKey: () => Promise<Uint8Array>;
|
||||
getFingerprint: (fingerprintMaterial: string, publicKey?: Uint8Array) => Promise<string[]>;
|
||||
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
|
||||
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
|
||||
getProviderKey: (providerId: string) => Promise<SymmetricCryptoKey>;
|
||||
@@ -63,7 +63,7 @@ export abstract class CryptoService {
|
||||
kdf: KdfType,
|
||||
kdfConfig: KdfConfig
|
||||
) => Promise<SymmetricCryptoKey>;
|
||||
makeSendKey: (keyMaterial: ArrayBuffer) => Promise<SymmetricCryptoKey>;
|
||||
makeSendKey: (keyMaterial: Uint8Array) => Promise<SymmetricCryptoKey>;
|
||||
hashPassword: (
|
||||
password: string,
|
||||
key: SymmetricCryptoKey,
|
||||
@@ -74,13 +74,13 @@ export abstract class CryptoService {
|
||||
key: SymmetricCryptoKey,
|
||||
encKey?: SymmetricCryptoKey
|
||||
) => Promise<[SymmetricCryptoKey, EncString]>;
|
||||
encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise<EncString>;
|
||||
encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise<EncArrayBuffer>;
|
||||
rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer) => Promise<EncString>;
|
||||
rsaDecrypt: (encValue: string, privateKeyValue?: ArrayBuffer) => Promise<ArrayBuffer>;
|
||||
decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
||||
encrypt: (plainValue: string | Uint8Array, key?: SymmetricCryptoKey) => Promise<EncString>;
|
||||
encryptToBytes: (plainValue: Uint8Array, key?: SymmetricCryptoKey) => Promise<EncArrayBuffer>;
|
||||
rsaEncrypt: (data: Uint8Array, publicKey?: Uint8Array) => Promise<EncString>;
|
||||
rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise<Uint8Array>;
|
||||
decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise<Uint8Array>;
|
||||
decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise<string>;
|
||||
decryptFromBytes: (encBuffer: EncArrayBuffer, key: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
||||
decryptFromBytes: (encBuffer: EncArrayBuffer, key: SymmetricCryptoKey) => Promise<Uint8Array>;
|
||||
randomNumber: (min: number, max: number) => Promise<number>;
|
||||
validateKey: (key: SymmetricCryptoKey) => Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import { EncString } from "../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class EncryptService {
|
||||
abstract encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString>;
|
||||
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
|
||||
abstract encryptToBytes: (
|
||||
plainValue: ArrayBuffer,
|
||||
plainValue: Uint8Array,
|
||||
key?: SymmetricCryptoKey
|
||||
) => Promise<EncArrayBuffer>;
|
||||
abstract decryptToUtf8: (encString: EncString, key: SymmetricCryptoKey) => Promise<string>;
|
||||
abstract decryptToBytes: (encThing: Encrypted, key: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
||||
abstract decryptToBytes: (encThing: Encrypted, key: SymmetricCryptoKey) => Promise<Uint8Array>;
|
||||
abstract resolveLegacyKey: (key: SymmetricCryptoKey, encThing: Encrypted) => SymmetricCryptoKey;
|
||||
abstract decryptItems: <T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
|
||||
@@ -17,12 +17,41 @@ export type PayPalConfig = {
|
||||
buttonAction?: string;
|
||||
};
|
||||
|
||||
export enum Region {
|
||||
US = "US",
|
||||
EU = "EU",
|
||||
SelfHosted = "Self-hosted",
|
||||
}
|
||||
|
||||
export enum RegionDomain {
|
||||
US = "bitwarden.com",
|
||||
EU = "bitwarden.eu",
|
||||
USQA = "bitwarden.pw",
|
||||
}
|
||||
|
||||
export abstract class EnvironmentService {
|
||||
urls: Observable<void>;
|
||||
usUrls: Urls;
|
||||
euUrls: Urls;
|
||||
selectedRegion?: Region;
|
||||
initialized = true;
|
||||
|
||||
hasBaseUrl: () => boolean;
|
||||
getNotificationsUrl: () => string;
|
||||
getWebVaultUrl: () => string;
|
||||
/**
|
||||
* Retrieves the URL of the cloud web vault app.
|
||||
*
|
||||
* @returns {string} The URL of the cloud web vault app.
|
||||
* @remarks Use this method only in views exclusive to self-host instances.
|
||||
*/
|
||||
getCloudWebVaultUrl: () => string;
|
||||
/**
|
||||
* Sets the URL of the cloud web vault app based on the region parameter.
|
||||
*
|
||||
* @param {Region} region - The region of the cloud web vault app.
|
||||
*/
|
||||
setCloudWebVaultUrl: (region: Region) => void;
|
||||
getSendUrl: () => string;
|
||||
getIconsUrl: () => string;
|
||||
getApiUrl: () => string;
|
||||
@@ -32,11 +61,8 @@ export abstract class EnvironmentService {
|
||||
getScimUrl: () => string;
|
||||
setUrlsFromStorage: () => Promise<void>;
|
||||
setUrls: (urls: Urls) => Promise<Urls>;
|
||||
setRegion: (region: Region) => Promise<void>;
|
||||
getUrls: () => Urls;
|
||||
isCloud: () => boolean;
|
||||
/**
|
||||
* @remarks For desktop and browser use only.
|
||||
* For web, use PlatformUtilsService.isSelfHost()
|
||||
*/
|
||||
isSelfHosted: () => boolean;
|
||||
isEmpty: () => boolean;
|
||||
}
|
||||
|
||||
@@ -113,8 +113,8 @@ export abstract class StateService<T extends Account = Account> {
|
||||
* @deprecated Do not call this, use PolicyService
|
||||
*/
|
||||
setDecryptedPolicies: (value: Policy[], options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedPrivateKey: (options?: StorageOptions) => Promise<ArrayBuffer>;
|
||||
setDecryptedPrivateKey: (value: ArrayBuffer, options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedPrivateKey: (options?: StorageOptions) => Promise<Uint8Array>;
|
||||
setDecryptedPrivateKey: (value: Uint8Array, options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedProviderKeys: (options?: StorageOptions) => Promise<Map<string, SymmetricCryptoKey>>;
|
||||
setDecryptedProviderKeys: (
|
||||
value: Map<string, SymmetricCryptoKey>,
|
||||
@@ -263,6 +263,8 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setEntityType: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEnvironmentUrls: (options?: StorageOptions) => Promise<EnvironmentUrls>;
|
||||
setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise<void>;
|
||||
getRegion: (options?: StorageOptions) => Promise<string>;
|
||||
setRegion: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>;
|
||||
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
|
||||
@@ -329,8 +331,8 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getProviders: (options?: StorageOptions) => Promise<{ [id: string]: ProviderData }>;
|
||||
setProviders: (value: { [id: string]: ProviderData }, options?: StorageOptions) => Promise<void>;
|
||||
getPublicKey: (options?: StorageOptions) => Promise<ArrayBuffer>;
|
||||
setPublicKey: (value: ArrayBuffer, options?: StorageOptions) => Promise<void>;
|
||||
getPublicKey: (options?: StorageOptions) => Promise<Uint8Array>;
|
||||
setPublicKey: (value: Uint8Array, options?: StorageOptions) => Promise<void>;
|
||||
getRefreshToken: (options?: StorageOptions) => Promise<string>;
|
||||
setRefreshToken: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getRememberedEmail: (options?: StorageOptions) => Promise<string>;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { EncryptionType } from "../../enums";
|
||||
|
||||
export interface Encrypted {
|
||||
encryptionType?: EncryptionType;
|
||||
dataBytes: ArrayBuffer;
|
||||
macBytes: ArrayBuffer;
|
||||
ivBytes: ArrayBuffer;
|
||||
dataBytes: Uint8Array;
|
||||
macBytes: Uint8Array;
|
||||
ivBytes: Uint8Array;
|
||||
}
|
||||
|
||||
@@ -358,4 +358,32 @@ describe("Utils Service", () => {
|
||||
expect(actual.protocol).toBe("http:");
|
||||
});
|
||||
});
|
||||
|
||||
describe("daysRemaining", () => {
|
||||
beforeAll(() => {
|
||||
const now = new Date(2023, 9, 2, 10);
|
||||
jest.spyOn(Date, "now").mockReturnValue(now.getTime());
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should return 0 for equal dates", () => {
|
||||
expect(Utils.daysRemaining(new Date(2023, 9, 2))).toBe(0);
|
||||
expect(Utils.daysRemaining(new Date(2023, 9, 2, 12))).toBe(0);
|
||||
});
|
||||
|
||||
it("should return 0 for dates in the past", () => {
|
||||
expect(Utils.daysRemaining(new Date(2020, 5, 11))).toBe(0);
|
||||
expect(Utils.daysRemaining(new Date(2023, 9, 1))).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle future dates", () => {
|
||||
expect(Utils.daysRemaining(new Date(2023, 9, 3, 10))).toBe(1);
|
||||
expect(Utils.daysRemaining(new Date(2023, 10, 12, 10))).toBe(41);
|
||||
// leap year
|
||||
expect(Utils.daysRemaining(new Date(2024, 9, 2, 10))).toBe(366);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -545,6 +545,16 @@ export class Utils {
|
||||
return of(undefined).pipe(switchMap(() => generator()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of days remaining before a target date arrives.
|
||||
* Returns 0 if the day has already passed.
|
||||
*/
|
||||
static daysRemaining(targetDate: Date): number {
|
||||
const diffTime = targetDate.getTime() - Date.now();
|
||||
const msPerDay = 86400000;
|
||||
return Math.max(0, Math.floor(diffTime / msPerDay));
|
||||
}
|
||||
|
||||
private static isAppleMobile(win: Window) {
|
||||
return (
|
||||
win.navigator.userAgent.match(/iPhone/i) != null ||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Region } from "../../abstractions/environment.service";
|
||||
|
||||
import {
|
||||
EnvironmentServerConfigData,
|
||||
ServerConfigData,
|
||||
@@ -15,6 +17,7 @@ describe("ServerConfigData", () => {
|
||||
url: "https://test.com",
|
||||
},
|
||||
environment: {
|
||||
cloudRegion: Region.EU,
|
||||
vault: "https://vault.com",
|
||||
api: "https://api.com",
|
||||
identity: "https://identity.com",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Region } from "../../abstractions/environment.service";
|
||||
import {
|
||||
ServerConfigResponse,
|
||||
ThirdPartyServerConfigResponse,
|
||||
@@ -50,6 +51,7 @@ export class ThirdPartyServerConfigData {
|
||||
}
|
||||
|
||||
export class EnvironmentServerConfigData {
|
||||
cloudRegion: Region;
|
||||
vault: string;
|
||||
api: string;
|
||||
identity: string;
|
||||
@@ -57,6 +59,7 @@ export class EnvironmentServerConfigData {
|
||||
sso: string;
|
||||
|
||||
constructor(response: Partial<EnvironmentServerConfigResponse>) {
|
||||
this.cloudRegion = response.cloudRegion;
|
||||
this.vault = response.vault;
|
||||
this.api = response.api;
|
||||
this.identity = response.identity;
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("AccountKeys", () => {
|
||||
describe("toJSON", () => {
|
||||
it("should serialize itself", () => {
|
||||
const keys = new AccountKeys();
|
||||
const buffer = makeStaticByteArray(64).buffer;
|
||||
const buffer = makeStaticByteArray(64);
|
||||
keys.publicKey = buffer;
|
||||
|
||||
const bufferSpy = jest.spyOn(Utils, "fromBufferToByteString");
|
||||
@@ -18,7 +18,7 @@ describe("AccountKeys", () => {
|
||||
|
||||
it("should serialize public key as a string", () => {
|
||||
const keys = new AccountKeys();
|
||||
keys.publicKey = Utils.fromByteStringToArray("hello").buffer;
|
||||
keys.publicKey = Utils.fromByteStringToArray("hello");
|
||||
const json = JSON.stringify(keys);
|
||||
expect(json).toContain('"publicKey":"hello"');
|
||||
});
|
||||
@@ -29,7 +29,7 @@ describe("AccountKeys", () => {
|
||||
const keys = AccountKeys.fromJSON({
|
||||
publicKey: "hello",
|
||||
});
|
||||
expect(keys.publicKey).toEqual(Utils.fromByteStringToArray("hello").buffer);
|
||||
expect(keys.publicKey).toEqual(Utils.fromByteStringToArray("hello"));
|
||||
});
|
||||
|
||||
it("should deserialize cryptoMasterKey", () => {
|
||||
|
||||
@@ -119,8 +119,8 @@ export class AccountKeys {
|
||||
any,
|
||||
Record<string, SymmetricCryptoKey>
|
||||
>();
|
||||
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
|
||||
publicKey?: ArrayBuffer;
|
||||
privateKey?: EncryptionPair<string, Uint8Array> = new EncryptionPair<string, Uint8Array>();
|
||||
publicKey?: Uint8Array;
|
||||
apiKeyClientSecret?: string;
|
||||
|
||||
toJSON() {
|
||||
@@ -142,11 +142,10 @@ export class AccountKeys {
|
||||
),
|
||||
organizationKeys: AccountKeys.initRecordEncryptionPairsFromJSON(obj?.organizationKeys),
|
||||
providerKeys: AccountKeys.initRecordEncryptionPairsFromJSON(obj?.providerKeys),
|
||||
privateKey: EncryptionPair.fromJSON<string, ArrayBuffer>(
|
||||
obj?.privateKey,
|
||||
(decObj: string) => Utils.fromByteStringToArray(decObj).buffer
|
||||
privateKey: EncryptionPair.fromJSON<string, Uint8Array>(obj?.privateKey, (decObj: string) =>
|
||||
Utils.fromByteStringToArray(decObj)
|
||||
),
|
||||
publicKey: Utils.fromByteStringToArray(obj?.publicKey)?.buffer,
|
||||
publicKey: Utils.fromByteStringToArray(obj?.publicKey),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,6 +232,7 @@ export class AccountSettings {
|
||||
approveLoginRequests?: boolean;
|
||||
avatarColor?: string;
|
||||
activateAutoFillOnPageLoadFromPolicy?: boolean;
|
||||
region?: string;
|
||||
smOnboardingTasks?: Record<string, Record<string, boolean>>;
|
||||
|
||||
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("encArrayBuffer", () => {
|
||||
array.set(mac, 1 + iv.byteLength);
|
||||
array.set(data, 1 + iv.byteLength + mac.byteLength);
|
||||
|
||||
const actual = new EncArrayBuffer(array.buffer);
|
||||
const actual = new EncArrayBuffer(array);
|
||||
|
||||
expect(actual.encryptionType).toEqual(encType);
|
||||
expect(actual.ivBytes).toEqualBuffer(iv);
|
||||
@@ -39,11 +39,11 @@ describe("encArrayBuffer", () => {
|
||||
array.set(iv, 1);
|
||||
array.set(data, 1 + iv.byteLength);
|
||||
|
||||
const actual = new EncArrayBuffer(array.buffer);
|
||||
const actual = new EncArrayBuffer(array);
|
||||
|
||||
expect(actual.encryptionType).toEqual(encType);
|
||||
expect(actual.ivBytes).toEqualBuffer(iv);
|
||||
expect(actual.dataBytes).toEqualBuffer(data);
|
||||
expect(actual.ivBytes).toEqual(iv);
|
||||
expect(actual.dataBytes).toEqual(data);
|
||||
expect(actual.macBytes).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -58,13 +58,11 @@ describe("encArrayBuffer", () => {
|
||||
// Minus 1 to leave room for the encType, minus 1 to make it invalid
|
||||
const invalidBytes = makeStaticByteArray(minLength - 2);
|
||||
|
||||
const invalidArray = new Uint8Array(1 + invalidBytes.buffer.byteLength);
|
||||
const invalidArray = new Uint8Array(1 + invalidBytes.byteLength);
|
||||
invalidArray.set([encType]);
|
||||
invalidArray.set(invalidBytes, 1);
|
||||
|
||||
expect(() => new EncArrayBuffer(invalidArray.buffer)).toThrow(
|
||||
"Error parsing encrypted ArrayBuffer"
|
||||
);
|
||||
expect(() => new EncArrayBuffer(invalidArray)).toThrow("Error parsing encrypted ArrayBuffer");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ const MIN_DATA_LENGTH = 1;
|
||||
|
||||
export class EncArrayBuffer implements Encrypted {
|
||||
readonly encryptionType: EncryptionType = null;
|
||||
readonly dataBytes: ArrayBuffer = null;
|
||||
readonly ivBytes: ArrayBuffer = null;
|
||||
readonly macBytes: ArrayBuffer = null;
|
||||
readonly dataBytes: Uint8Array = null;
|
||||
readonly ivBytes: Uint8Array = null;
|
||||
readonly macBytes: Uint8Array = null;
|
||||
|
||||
constructor(readonly buffer: ArrayBuffer) {
|
||||
const encBytes = new Uint8Array(buffer);
|
||||
constructor(readonly buffer: Uint8Array) {
|
||||
const encBytes = buffer;
|
||||
const encType = encBytes[0];
|
||||
|
||||
switch (encType) {
|
||||
@@ -25,12 +25,12 @@ export class EncArrayBuffer implements Encrypted {
|
||||
this.throwDecryptionError();
|
||||
}
|
||||
|
||||
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH).buffer;
|
||||
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH);
|
||||
this.macBytes = encBytes.slice(
|
||||
ENC_TYPE_LENGTH + IV_LENGTH,
|
||||
ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH
|
||||
).buffer;
|
||||
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH).buffer;
|
||||
);
|
||||
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH);
|
||||
break;
|
||||
}
|
||||
case EncryptionType.AesCbc256_B64: {
|
||||
@@ -39,8 +39,8 @@ export class EncArrayBuffer implements Encrypted {
|
||||
this.throwDecryptionError();
|
||||
}
|
||||
|
||||
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH).buffer;
|
||||
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH).buffer;
|
||||
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH);
|
||||
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -63,11 +63,11 @@ export class EncArrayBuffer implements Encrypted {
|
||||
if (buffer == null) {
|
||||
throw new Error("Cannot create EncArrayBuffer from Response - Response is empty");
|
||||
}
|
||||
return new EncArrayBuffer(buffer);
|
||||
return new EncArrayBuffer(new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
static fromB64(b64: string) {
|
||||
const buffer = Utils.fromB64ToArray(b64).buffer;
|
||||
const buffer = Utils.fromB64ToArray(b64);
|
||||
return new EncArrayBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,16 +27,16 @@ export class EncString implements Encrypted {
|
||||
}
|
||||
}
|
||||
|
||||
get ivBytes(): ArrayBuffer {
|
||||
return this.iv == null ? null : Utils.fromB64ToArray(this.iv).buffer;
|
||||
get ivBytes(): Uint8Array {
|
||||
return this.iv == null ? null : Utils.fromB64ToArray(this.iv);
|
||||
}
|
||||
|
||||
get macBytes(): ArrayBuffer {
|
||||
return this.mac == null ? null : Utils.fromB64ToArray(this.mac).buffer;
|
||||
get macBytes(): Uint8Array {
|
||||
return this.mac == null ? null : Utils.fromB64ToArray(this.mac);
|
||||
}
|
||||
|
||||
get dataBytes(): ArrayBuffer {
|
||||
return this.data == null ? null : Utils.fromB64ToArray(this.data).buffer;
|
||||
get dataBytes(): Uint8Array {
|
||||
return this.data == null ? null : Utils.fromB64ToArray(this.data);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export class EncryptedObject {
|
||||
iv: ArrayBuffer;
|
||||
data: ArrayBuffer;
|
||||
mac: ArrayBuffer;
|
||||
iv: Uint8Array;
|
||||
data: Uint8Array;
|
||||
mac: Uint8Array;
|
||||
key: SymmetricCryptoKey;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,13 @@ describe("EncryptionPair", () => {
|
||||
expect(json.decrypted).toEqual("hello");
|
||||
});
|
||||
|
||||
it("should populate decryptedSerialized for TypesArrays", () => {
|
||||
const pair = new EncryptionPair<string, Uint8Array>();
|
||||
pair.decrypted = Utils.fromByteStringToArray("hello");
|
||||
const json = pair.toJSON();
|
||||
expect(json.decrypted).toEqual(new Uint8Array([104, 101, 108, 108, 111]));
|
||||
});
|
||||
|
||||
it("should serialize encrypted and decrypted", () => {
|
||||
const pair = new EncryptionPair<string, string>();
|
||||
pair.encrypted = "hello";
|
||||
|
||||
@@ -36,4 +36,5 @@ export class GlobalState {
|
||||
enableBrowserIntegration?: boolean;
|
||||
enableBrowserIntegrationFingerprint?: boolean;
|
||||
enableDuckDuckGoBrowserIntegration?: boolean;
|
||||
region?: string;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ describe("SymmetricCryptoKey", () => {
|
||||
});
|
||||
|
||||
it("toJSON creates object for serialization", () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64).buffer);
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const actual = key.toJSON();
|
||||
|
||||
const expected = { keyB64: key.keyB64 };
|
||||
@@ -77,7 +77,7 @@ describe("SymmetricCryptoKey", () => {
|
||||
});
|
||||
|
||||
it("fromJSON hydrates new object", () => {
|
||||
const expected = new SymmetricCryptoKey(makeStaticByteArray(64).buffer);
|
||||
const expected = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const actual = SymmetricCryptoKey.fromJSON({ keyB64: expected.keyB64 });
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
|
||||
@@ -4,9 +4,9 @@ import { EncryptionType } from "../../../enums";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
|
||||
export class SymmetricCryptoKey {
|
||||
key: ArrayBuffer;
|
||||
encKey?: ArrayBuffer;
|
||||
macKey?: ArrayBuffer;
|
||||
key: Uint8Array;
|
||||
encKey?: Uint8Array;
|
||||
macKey?: Uint8Array;
|
||||
encType: EncryptionType;
|
||||
|
||||
keyB64: string;
|
||||
@@ -15,7 +15,7 @@ export class SymmetricCryptoKey {
|
||||
|
||||
meta: any;
|
||||
|
||||
constructor(key: ArrayBuffer, encType?: EncryptionType) {
|
||||
constructor(key: Uint8Array, encType?: EncryptionType) {
|
||||
if (key == null) {
|
||||
throw new Error("Must provide key");
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export class SymmetricCryptoKey {
|
||||
return null;
|
||||
}
|
||||
|
||||
const arrayBuffer = Utils.fromB64ToArray(s).buffer;
|
||||
const arrayBuffer = Utils.fromB64ToArray(s);
|
||||
return new SymmetricCryptoKey(arrayBuffer);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { Region } from "../../abstractions/environment.service";
|
||||
|
||||
export class ServerConfigResponse extends BaseResponse {
|
||||
version: string;
|
||||
@@ -23,6 +24,7 @@ export class ServerConfigResponse extends BaseResponse {
|
||||
}
|
||||
|
||||
export class EnvironmentServerConfigResponse extends BaseResponse {
|
||||
cloudRegion: Region;
|
||||
vault: string;
|
||||
api: string;
|
||||
identity: string;
|
||||
@@ -36,6 +38,7 @@ export class EnvironmentServerConfigResponse extends BaseResponse {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cloudRegion = this.getResponseProperty("CloudRegion");
|
||||
this.vault = this.getResponseProperty("Vault");
|
||||
this.api = this.getResponseProperty("Api");
|
||||
this.identity = this.getResponseProperty("Identity");
|
||||
|
||||
@@ -44,6 +44,7 @@ export class ConfigService implements ConfigServiceAbstraction {
|
||||
return serverConfig;
|
||||
}
|
||||
await this.stateService.setServerConfig(data);
|
||||
this.environmentService.setCloudWebVaultUrl(data.environment?.cloudRegion);
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
@@ -62,6 +63,11 @@ export class ConfigService implements ConfigServiceAbstraction {
|
||||
return await this.getFeatureFlag(key, defaultValue);
|
||||
}
|
||||
|
||||
async getCloudRegion(defaultValue = "US"): Promise<string> {
|
||||
const serverConfig = await this.buildServerConfig();
|
||||
return serverConfig.environment?.cloudRegion ?? defaultValue;
|
||||
}
|
||||
|
||||
private async getFeatureFlag<T>(key: FeatureFlag, defaultValue: T): Promise<T> {
|
||||
const serverConfig = await this.buildServerConfig();
|
||||
if (
|
||||
|
||||
@@ -123,7 +123,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
const key = await this.retrieveKeyFromStorage(keySuffix, userId);
|
||||
if (key != null) {
|
||||
const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer);
|
||||
const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key));
|
||||
|
||||
if (!(await this.validateKey(symmetricKey))) {
|
||||
this.logService.warning("Wrong key, throwing away stored key");
|
||||
@@ -172,7 +172,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return this.getEncKeyHelper(key);
|
||||
}
|
||||
|
||||
async getPublicKey(): Promise<ArrayBuffer> {
|
||||
async getPublicKey(): Promise<Uint8Array> {
|
||||
const inMemoryPublicKey = await this.stateService.getPublicKey();
|
||||
if (inMemoryPublicKey != null) {
|
||||
return inMemoryPublicKey;
|
||||
@@ -188,7 +188,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
async getPrivateKey(): Promise<ArrayBuffer> {
|
||||
async getPrivateKey(): Promise<Uint8Array> {
|
||||
const decryptedPrivateKey = await this.stateService.getDecryptedPrivateKey();
|
||||
if (decryptedPrivateKey != null) {
|
||||
return decryptedPrivateKey;
|
||||
@@ -204,7 +204,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
async getFingerprint(userId: string, publicKey?: ArrayBuffer): Promise<string[]> {
|
||||
async getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]> {
|
||||
if (publicKey == null) {
|
||||
publicKey = await this.getPublicKey();
|
||||
}
|
||||
@@ -214,7 +214,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256");
|
||||
const userFingerprint = await this.cryptoFunctionService.hkdfExpand(
|
||||
keyFingerprint,
|
||||
userId,
|
||||
fingerprintMaterial,
|
||||
32,
|
||||
"sha256"
|
||||
);
|
||||
@@ -416,7 +416,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
kdf: KdfType,
|
||||
kdfConfig: KdfConfig
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
let key: ArrayBuffer = null;
|
||||
let key: Uint8Array = null;
|
||||
if (kdf == null || kdf === KdfType.PBKDF2_SHA256) {
|
||||
if (kdfConfig.iterations == null) {
|
||||
kdfConfig.iterations = 5000;
|
||||
@@ -502,7 +502,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return await this.stretchKey(pinKey);
|
||||
}
|
||||
|
||||
async makeSendKey(keyMaterial: ArrayBuffer): Promise<SymmetricCryptoKey> {
|
||||
async makeSendKey(keyMaterial: Uint8Array): Promise<SymmetricCryptoKey> {
|
||||
const sendKey = await this.cryptoFunctionService.hkdf(
|
||||
keyMaterial,
|
||||
"bitwarden-send",
|
||||
@@ -550,7 +550,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
|
||||
* and then call encryptService.encrypt
|
||||
*/
|
||||
async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncString> {
|
||||
async encrypt(plainValue: string | Uint8Array, key?: SymmetricCryptoKey): Promise<EncString> {
|
||||
key = await this.getKeyForUserEncryption(key);
|
||||
return await this.encryptService.encrypt(plainValue, key);
|
||||
}
|
||||
@@ -559,12 +559,12 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
|
||||
* and then call encryptService.encryptToBytes
|
||||
*/
|
||||
async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||
async encryptToBytes(plainValue: Uint8Array, key?: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||
key = await this.getKeyForUserEncryption(key);
|
||||
return this.encryptService.encryptToBytes(plainValue, key);
|
||||
}
|
||||
|
||||
async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer): Promise<EncString> {
|
||||
async rsaEncrypt(data: Uint8Array, publicKey?: Uint8Array): Promise<EncString> {
|
||||
if (publicKey == null) {
|
||||
publicKey = await this.getPublicKey();
|
||||
}
|
||||
@@ -576,7 +576,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes));
|
||||
}
|
||||
|
||||
async rsaDecrypt(encValue: string, privateKeyValue?: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
async rsaDecrypt(encValue: string, privateKeyValue?: Uint8Array): Promise<Uint8Array> {
|
||||
const headerPieces = encValue.split(".");
|
||||
let encType: EncryptionType = null;
|
||||
let encPieces: string[];
|
||||
@@ -607,7 +607,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
throw new Error("encPieces unavailable.");
|
||||
}
|
||||
|
||||
const data = Utils.fromB64ToArray(encPieces[0]).buffer;
|
||||
const data = Utils.fromB64ToArray(encPieces[0]);
|
||||
const privateKey = privateKeyValue ?? (await this.getPrivateKey());
|
||||
if (privateKey == null) {
|
||||
throw new Error("No private key.");
|
||||
@@ -633,7 +633,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
|
||||
* and then call encryptService.decryptToBytes
|
||||
*/
|
||||
async decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||
async decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise<Uint8Array> {
|
||||
const keyForEnc = await this.getKeyForUserEncryption(key);
|
||||
return this.encryptService.decryptToBytes(encString, keyForEnc);
|
||||
}
|
||||
@@ -651,7 +651,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
|
||||
* and then call encryptService.decryptToBytes
|
||||
*/
|
||||
async decryptFromBytes(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||
async decryptFromBytes(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<Uint8Array> {
|
||||
if (encBuffer == null) {
|
||||
throw new Error("No buffer provided for decryption.");
|
||||
}
|
||||
@@ -768,10 +768,10 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256");
|
||||
newKey.set(new Uint8Array(encKey));
|
||||
newKey.set(new Uint8Array(macKey), 32);
|
||||
return new SymmetricCryptoKey(newKey.buffer);
|
||||
return new SymmetricCryptoKey(newKey);
|
||||
}
|
||||
|
||||
private async hashPhrase(hash: ArrayBuffer, minimumEntropy = 64) {
|
||||
private async hashPhrase(hash: Uint8Array, minimumEntropy = 64) {
|
||||
const entropyPerWord = Math.log(EFFLongWordList.length) / Math.log(2);
|
||||
let numWords = Math.ceil(minimumEntropy / entropyPerWord);
|
||||
|
||||
@@ -793,7 +793,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
|
||||
private async buildEncKey(
|
||||
key: SymmetricCryptoKey,
|
||||
encKey: ArrayBuffer
|
||||
encKey: Uint8Array
|
||||
): Promise<[SymmetricCryptoKey, EncString]> {
|
||||
let encKeyEnc: EncString = null;
|
||||
if (key.key.byteLength === 32) {
|
||||
@@ -830,7 +830,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return null;
|
||||
}
|
||||
|
||||
let decEncKey: ArrayBuffer;
|
||||
let decEncKey: Uint8Array;
|
||||
const encKeyCipher = new EncString(encKey);
|
||||
if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
decEncKey = await this.decryptToBytes(encKeyCipher, key);
|
||||
|
||||
@@ -18,7 +18,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
protected logMacFailures: boolean
|
||||
) {}
|
||||
|
||||
async encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
@@ -27,9 +27,9 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
let plainBuf: ArrayBuffer;
|
||||
let plainBuf: Uint8Array;
|
||||
if (typeof plainValue === "string") {
|
||||
plainBuf = Utils.fromUtf8ToArray(plainValue).buffer;
|
||||
plainBuf = Utils.fromUtf8ToArray(plainValue);
|
||||
} else {
|
||||
plainBuf = plainValue;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
return new EncString(encObj.key.encType, data, iv, mac);
|
||||
}
|
||||
|
||||
async encryptToBytes(plainValue: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||
async encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
}
|
||||
|
||||
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
|
||||
return new EncArrayBuffer(encBytes.buffer);
|
||||
return new EncArrayBuffer(encBytes);
|
||||
}
|
||||
|
||||
async decryptToUtf8(encString: EncString, key: SymmetricCryptoKey): Promise<string> {
|
||||
@@ -102,7 +102,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
return await this.cryptoFunctionService.aesDecryptFast(fastParams);
|
||||
}
|
||||
|
||||
async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||
async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise<Uint8Array> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
@@ -125,11 +125,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
const macData = new Uint8Array(encThing.ivBytes.byteLength + encThing.dataBytes.byteLength);
|
||||
macData.set(new Uint8Array(encThing.ivBytes), 0);
|
||||
macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength);
|
||||
const computedMac = await this.cryptoFunctionService.hmac(
|
||||
macData.buffer,
|
||||
key.macKey,
|
||||
"sha256"
|
||||
);
|
||||
const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256");
|
||||
if (computedMac === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -161,7 +157,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
return await Promise.all(items.map((item) => item.decrypt(key)));
|
||||
}
|
||||
|
||||
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
||||
private async aesEncrypt(data: Uint8Array, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
||||
const obj = new EncryptedObject();
|
||||
obj.key = key;
|
||||
obj.iv = await this.cryptoFunctionService.randomBytes(16);
|
||||
@@ -171,7 +167,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength);
|
||||
macData.set(new Uint8Array(obj.iv), 0);
|
||||
macData.set(new Uint8Array(obj.data), obj.iv.byteLength);
|
||||
obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, "sha256");
|
||||
obj.mac = await this.cryptoFunctionService.hmac(macData, obj.key.macKey, "sha256");
|
||||
}
|
||||
|
||||
return obj;
|
||||
|
||||
@@ -37,10 +37,8 @@ describe("EncryptService", () => {
|
||||
|
||||
describe("encrypts data", () => {
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService.randomBytes
|
||||
.calledWith(16)
|
||||
.mockResolvedValueOnce(iv.buffer as CsprngArray);
|
||||
cryptoFunctionService.aesEncrypt.mockResolvedValue(encryptedData.buffer);
|
||||
cryptoFunctionService.randomBytes.calledWith(16).mockResolvedValueOnce(iv as CsprngArray);
|
||||
cryptoFunctionService.aesEncrypt.mockResolvedValue(encryptedData);
|
||||
});
|
||||
|
||||
it("using a key which supports mac", async () => {
|
||||
@@ -50,7 +48,7 @@ describe("EncryptService", () => {
|
||||
|
||||
key.macKey = makeStaticByteArray(16, 20);
|
||||
|
||||
cryptoFunctionService.hmac.mockResolvedValue(mac.buffer);
|
||||
cryptoFunctionService.hmac.mockResolvedValue(mac);
|
||||
|
||||
const actual = await encryptService.encryptToBytes(plainValue, key);
|
||||
|
||||
@@ -86,7 +84,7 @@ describe("EncryptService", () => {
|
||||
describe("decryptToBytes", () => {
|
||||
const encType = EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 100), encType);
|
||||
const computedMac = new Uint8Array(1).buffer;
|
||||
const computedMac = new Uint8Array(1);
|
||||
const encBuffer = new EncArrayBuffer(makeStaticByteArray(60, encType));
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -106,9 +104,9 @@ describe("EncryptService", () => {
|
||||
});
|
||||
|
||||
it("decrypts data with provided key", async () => {
|
||||
const decryptedBytes = makeStaticByteArray(10, 200).buffer;
|
||||
const decryptedBytes = makeStaticByteArray(10, 200);
|
||||
|
||||
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(1).buffer);
|
||||
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(1));
|
||||
cryptoFunctionService.compare.mockResolvedValue(true);
|
||||
cryptoFunctionService.aesDecrypt.mockResolvedValueOnce(decryptedBytes);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { concatMap, Observable, Subject } from "rxjs";
|
||||
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
||||
import {
|
||||
EnvironmentService as EnvironmentServiceAbstraction,
|
||||
Region,
|
||||
Urls,
|
||||
} from "../abstractions/environment.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
@@ -10,6 +11,8 @@ import { StateService } from "../abstractions/state.service";
|
||||
export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
private readonly urlsSubject = new Subject<void>();
|
||||
urls: Observable<void> = this.urlsSubject.asObservable();
|
||||
selectedRegion?: Region;
|
||||
initialized = false;
|
||||
|
||||
protected baseUrl: string;
|
||||
protected webVaultUrl: string;
|
||||
@@ -20,11 +23,37 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
protected eventsUrl: string;
|
||||
private keyConnectorUrl: string;
|
||||
private scimUrl: string = null;
|
||||
private cloudWebVaultUrl: string;
|
||||
|
||||
readonly usUrls: Urls = {
|
||||
base: null,
|
||||
api: "https://api.bitwarden.com",
|
||||
identity: "https://identity.bitwarden.com",
|
||||
icons: "https://icons.bitwarden.net",
|
||||
webVault: "https://vault.bitwarden.com",
|
||||
notifications: "https://notifications.bitwarden.com",
|
||||
events: "https://events.bitwarden.com",
|
||||
scim: "https://scim.bitwarden.com",
|
||||
};
|
||||
|
||||
readonly euUrls: Urls = {
|
||||
base: null,
|
||||
api: "https://api.bitwarden.eu",
|
||||
identity: "https://identity.bitwarden.eu",
|
||||
icons: "https://icons.bitwarden.eu",
|
||||
webVault: "https://vault.bitwarden.eu",
|
||||
notifications: "https://notifications.bitwarden.eu",
|
||||
events: "https://events.bitwarden.eu",
|
||||
scim: "https://scim.bitwarden.eu",
|
||||
};
|
||||
|
||||
constructor(private stateService: StateService) {
|
||||
this.stateService.activeAccount$
|
||||
.pipe(
|
||||
concatMap(async () => {
|
||||
if (!this.initialized) {
|
||||
return;
|
||||
}
|
||||
await this.setUrlsFromStorage();
|
||||
})
|
||||
)
|
||||
@@ -58,6 +87,26 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
return "https://vault.bitwarden.com";
|
||||
}
|
||||
|
||||
getCloudWebVaultUrl() {
|
||||
if (this.cloudWebVaultUrl != null) {
|
||||
return this.cloudWebVaultUrl;
|
||||
}
|
||||
|
||||
return this.usUrls.webVault;
|
||||
}
|
||||
|
||||
setCloudWebVaultUrl(region: Region) {
|
||||
switch (region) {
|
||||
case Region.EU:
|
||||
this.cloudWebVaultUrl = this.euUrls.webVault;
|
||||
break;
|
||||
case Region.US:
|
||||
default:
|
||||
this.cloudWebVaultUrl = this.usUrls.webVault;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getSendUrl() {
|
||||
return this.getWebVaultUrl() === "https://vault.bitwarden.com"
|
||||
? "https://send.bitwarden.com/#"
|
||||
@@ -127,20 +176,42 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
}
|
||||
|
||||
async setUrlsFromStorage(): Promise<void> {
|
||||
const urls: any = await this.stateService.getEnvironmentUrls();
|
||||
const region = await this.stateService.getRegion();
|
||||
const savedUrls = await this.stateService.getEnvironmentUrls();
|
||||
const envUrls = new EnvironmentUrls();
|
||||
|
||||
this.baseUrl = envUrls.base = urls.base;
|
||||
this.webVaultUrl = urls.webVault;
|
||||
this.apiUrl = envUrls.api = urls.api;
|
||||
this.identityUrl = envUrls.identity = urls.identity;
|
||||
this.iconsUrl = urls.icons;
|
||||
this.notificationsUrl = urls.notifications;
|
||||
this.eventsUrl = envUrls.events = urls.events;
|
||||
this.keyConnectorUrl = urls.keyConnector;
|
||||
// scimUrl is not saved to storage
|
||||
// In release `2023.5.0`, we set the `base` property of the environment URLs to the US web vault URL when a user clicked the "US" region.
|
||||
// This check will detect these cases and convert them to the proper region instead.
|
||||
// We are detecting this by checking for the presence of the web vault URL in the `base` and the absence of the `notifications` property.
|
||||
// This is because the `notifications` will not be `null` in the web vault, and we don't want to migrate the URLs in that case.
|
||||
if (savedUrls.base === "https://vault.bitwarden.com" && savedUrls.notifications == null) {
|
||||
await this.setRegion(Region.US);
|
||||
return;
|
||||
}
|
||||
|
||||
this.urlsSubject.next();
|
||||
switch (region) {
|
||||
case Region.EU:
|
||||
await this.setRegion(Region.EU);
|
||||
return;
|
||||
case Region.US:
|
||||
await this.setRegion(Region.US);
|
||||
return;
|
||||
case Region.SelfHosted:
|
||||
case null:
|
||||
default:
|
||||
this.baseUrl = envUrls.base = savedUrls.base;
|
||||
this.webVaultUrl = savedUrls.webVault;
|
||||
this.apiUrl = envUrls.api = savedUrls.api;
|
||||
this.identityUrl = envUrls.identity = savedUrls.identity;
|
||||
this.iconsUrl = savedUrls.icons;
|
||||
this.notificationsUrl = savedUrls.notifications;
|
||||
this.eventsUrl = envUrls.events = savedUrls.events;
|
||||
this.keyConnectorUrl = savedUrls.keyConnector;
|
||||
await this.setRegion(Region.SelfHosted);
|
||||
// scimUrl is not saved to storage
|
||||
this.urlsSubject.next();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async setUrls(urls: Urls): Promise<Urls> {
|
||||
@@ -178,6 +249,8 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
this.keyConnectorUrl = urls.keyConnector;
|
||||
this.scimUrl = urls.scim;
|
||||
|
||||
await this.setRegion(Region.SelfHosted);
|
||||
|
||||
this.urlsSubject.next();
|
||||
|
||||
return urls;
|
||||
@@ -187,6 +260,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
return {
|
||||
base: this.baseUrl,
|
||||
webVault: this.webVaultUrl,
|
||||
cloudWebVault: this.cloudWebVaultUrl,
|
||||
api: this.apiUrl,
|
||||
identity: this.identityUrl,
|
||||
icons: this.iconsUrl,
|
||||
@@ -197,6 +271,53 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
};
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return (
|
||||
this.baseUrl == null &&
|
||||
this.webVaultUrl == null &&
|
||||
this.apiUrl == null &&
|
||||
this.identityUrl == null &&
|
||||
this.iconsUrl == null &&
|
||||
this.notificationsUrl == null &&
|
||||
this.eventsUrl == null
|
||||
);
|
||||
}
|
||||
|
||||
async setRegion(region: Region) {
|
||||
this.selectedRegion = region;
|
||||
await this.stateService.setRegion(region);
|
||||
|
||||
if (region === Region.SelfHosted) {
|
||||
// If user saves a self-hosted region with empty fields, default to US
|
||||
if (this.isEmpty()) {
|
||||
await this.setRegion(Region.US);
|
||||
}
|
||||
} else {
|
||||
// If we are setting the region to EU or US, clear the self-hosted URLs
|
||||
await this.stateService.setEnvironmentUrls(new EnvironmentUrls());
|
||||
if (region === Region.EU) {
|
||||
this.setUrlsInternal(this.euUrls);
|
||||
} else if (region === Region.US) {
|
||||
this.setUrlsInternal(this.usUrls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setUrlsInternal(urls: Urls) {
|
||||
this.baseUrl = this.formatUrl(urls.base);
|
||||
this.webVaultUrl = this.formatUrl(urls.webVault);
|
||||
this.apiUrl = this.formatUrl(urls.api);
|
||||
this.identityUrl = this.formatUrl(urls.identity);
|
||||
this.iconsUrl = this.formatUrl(urls.icons);
|
||||
this.notificationsUrl = this.formatUrl(urls.notifications);
|
||||
this.eventsUrl = this.formatUrl(urls.events);
|
||||
this.keyConnectorUrl = this.formatUrl(urls.keyConnector);
|
||||
|
||||
// scimUrl cannot be cleared
|
||||
this.scimUrl = this.formatUrl(urls.scim) ?? this.scimUrl;
|
||||
this.urlsSubject.next();
|
||||
}
|
||||
|
||||
private formatUrl(url: string): string {
|
||||
if (url == null || url === "") {
|
||||
return null;
|
||||
@@ -211,19 +332,11 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
}
|
||||
|
||||
isCloud(): boolean {
|
||||
return ["https://api.bitwarden.com", "https://vault.bitwarden.com/api"].includes(
|
||||
this.getApiUrl()
|
||||
);
|
||||
}
|
||||
|
||||
isSelfHosted(): boolean {
|
||||
return ![
|
||||
"http://vault.bitwarden.com",
|
||||
"https://vault.bitwarden.com",
|
||||
"http://vault.bitwarden.eu",
|
||||
"https://vault.bitwarden.eu",
|
||||
"http://vault.qa.bitwarden.pw",
|
||||
"https://vault.qa.bitwarden.pw",
|
||||
].includes(this.getWebVaultUrl());
|
||||
return [
|
||||
"https://api.bitwarden.com",
|
||||
"https://vault.bitwarden.com/api",
|
||||
"https://api.bitwarden.eu",
|
||||
"https://vault.bitwarden.eu/api",
|
||||
].includes(this.getApiUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ export class StateService<
|
||||
}
|
||||
|
||||
async addAccount(account: TAccount) {
|
||||
account = await this.setAccountEnvironmentUrls(account);
|
||||
account = await this.setAccountEnvironment(account);
|
||||
await this.updateState(async (state) => {
|
||||
state.authenticatedAccounts.push(account.profile.userId);
|
||||
await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts);
|
||||
@@ -763,13 +763,13 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getDecryptedPrivateKey(options?: StorageOptions): Promise<ArrayBuffer> {
|
||||
async getDecryptedPrivateKey(options?: StorageOptions): Promise<Uint8Array> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
)?.keys?.privateKey.decrypted;
|
||||
}
|
||||
|
||||
async setDecryptedPrivateKey(value: ArrayBuffer, options?: StorageOptions): Promise<void> {
|
||||
async setDecryptedPrivateKey(value: Uint8Array, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||
);
|
||||
@@ -1598,6 +1598,28 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getRegion(options?: StorageOptions): Promise<string> {
|
||||
if ((await this.state())?.activeUserId == null) {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
|
||||
return (await this.getGlobals(options)).region ?? null;
|
||||
}
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
|
||||
return (await this.getAccount(options))?.settings?.region ?? null;
|
||||
}
|
||||
|
||||
async setRegion(value: string, options?: StorageOptions): Promise<void> {
|
||||
// Global values are set on each change and the current global settings are passed to any newly authed accounts.
|
||||
// This is to allow setting region values before an account is active, while still allowing individual accounts to have their own region.
|
||||
const globals = await this.getGlobals(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
globals.region = value;
|
||||
await this.saveGlobals(
|
||||
globals,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
}
|
||||
|
||||
async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
@@ -2075,14 +2097,14 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getPublicKey(options?: StorageOptions): Promise<ArrayBuffer> {
|
||||
async getPublicKey(options?: StorageOptions): Promise<Uint8Array> {
|
||||
const keys = (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
)?.keys;
|
||||
return keys?.publicKey;
|
||||
}
|
||||
|
||||
async setPublicKey(value: ArrayBuffer, options?: StorageOptions): Promise<void> {
|
||||
async setPublicKey(value: Uint8Array, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||
);
|
||||
@@ -2584,8 +2606,9 @@ export class StateService<
|
||||
await this.defaultOnDiskLocalOptions()
|
||||
)
|
||||
);
|
||||
// EnvironmentUrls are set before authenticating and should override whatever is stored from any previous session
|
||||
// EnvironmentUrls and region are set before authenticating and should override whatever is stored from any previous session
|
||||
const environmentUrls = account.settings.environmentUrls;
|
||||
const region = account.settings.region;
|
||||
if (storedAccount?.settings != null) {
|
||||
account.settings = storedAccount.settings;
|
||||
} else if (await this.storageService.has(keys.tempAccountSettings)) {
|
||||
@@ -2593,6 +2616,8 @@ export class StateService<
|
||||
await this.storageService.remove(keys.tempAccountSettings);
|
||||
}
|
||||
account.settings.environmentUrls = environmentUrls;
|
||||
account.settings.region = region;
|
||||
|
||||
if (
|
||||
account.settings.vaultTimeoutAction === VaultTimeoutAction.LogOut &&
|
||||
account.settings.vaultTimeout != null
|
||||
@@ -2620,6 +2645,7 @@ export class StateService<
|
||||
);
|
||||
if (storedAccount?.settings != null) {
|
||||
storedAccount.settings.environmentUrls = account.settings.environmentUrls;
|
||||
storedAccount.settings.region = account.settings.region;
|
||||
account.settings = storedAccount.settings;
|
||||
}
|
||||
await this.storageService.save(
|
||||
@@ -2642,6 +2668,7 @@ export class StateService<
|
||||
);
|
||||
if (storedAccount?.settings != null) {
|
||||
storedAccount.settings.environmentUrls = account.settings.environmentUrls;
|
||||
storedAccount.settings.region = account.settings.region;
|
||||
account.settings = storedAccount.settings;
|
||||
}
|
||||
await this.storageService.save(
|
||||
@@ -2790,7 +2817,9 @@ export class StateService<
|
||||
return Object.assign(this.createAccount(), persistentAccountInformation);
|
||||
}
|
||||
|
||||
protected async setAccountEnvironmentUrls(account: TAccount): Promise<TAccount> {
|
||||
// The environment urls and region are selected before login and are transferred here to an authenticated account
|
||||
protected async setAccountEnvironment(account: TAccount): Promise<TAccount> {
|
||||
account.settings.region = await this.getGlobalRegion();
|
||||
account.settings.environmentUrls = await this.getGlobalEnvironmentUrls();
|
||||
return account;
|
||||
}
|
||||
@@ -2800,6 +2829,11 @@ export class StateService<
|
||||
return (await this.getGlobals(options)).environmentUrls ?? new EnvironmentUrls();
|
||||
}
|
||||
|
||||
protected async getGlobalRegion(options?: StorageOptions): Promise<string> {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
|
||||
return (await this.getGlobals(options)).region ?? null;
|
||||
}
|
||||
|
||||
protected async clearDecryptedDataForActiveUser(): Promise<void> {
|
||||
await this.updateState(async (state) => {
|
||||
const userId = state?.activeUserId;
|
||||
|
||||
@@ -160,7 +160,7 @@ describe("WebCrypto Function Service", () => {
|
||||
const a = new Uint8Array(2);
|
||||
a[0] = 1;
|
||||
a[1] = 2;
|
||||
const equal = await cryptoFunctionService.compare(a.buffer, a.buffer);
|
||||
const equal = await cryptoFunctionService.compare(a, a);
|
||||
expect(equal).toBe(true);
|
||||
});
|
||||
|
||||
@@ -172,7 +172,7 @@ describe("WebCrypto Function Service", () => {
|
||||
const b = new Uint8Array(2);
|
||||
b[0] = 3;
|
||||
b[1] = 4;
|
||||
const equal = await cryptoFunctionService.compare(a.buffer, b.buffer);
|
||||
const equal = await cryptoFunctionService.compare(a, b);
|
||||
expect(equal).toBe(false);
|
||||
});
|
||||
|
||||
@@ -183,7 +183,7 @@ describe("WebCrypto Function Service", () => {
|
||||
a[1] = 2;
|
||||
const b = new Uint8Array(2);
|
||||
b[0] = 3;
|
||||
const equal = await cryptoFunctionService.compare(a.buffer, b.buffer);
|
||||
const equal = await cryptoFunctionService.compare(a, b);
|
||||
expect(equal).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -200,7 +200,7 @@ describe("WebCrypto Function Service", () => {
|
||||
const a = new Uint8Array(2);
|
||||
a[0] = 1;
|
||||
a[1] = 2;
|
||||
const aByteString = Utils.fromBufferToByteString(a.buffer);
|
||||
const aByteString = Utils.fromBufferToByteString(a);
|
||||
const equal = await cryptoFunctionService.compareFast(aByteString, aByteString);
|
||||
expect(equal).toBe(true);
|
||||
});
|
||||
@@ -210,11 +210,11 @@ describe("WebCrypto Function Service", () => {
|
||||
const a = new Uint8Array(2);
|
||||
a[0] = 1;
|
||||
a[1] = 2;
|
||||
const aByteString = Utils.fromBufferToByteString(a.buffer);
|
||||
const aByteString = Utils.fromBufferToByteString(a);
|
||||
const b = new Uint8Array(2);
|
||||
b[0] = 3;
|
||||
b[1] = 4;
|
||||
const bByteString = Utils.fromBufferToByteString(b.buffer);
|
||||
const bByteString = Utils.fromBufferToByteString(b);
|
||||
const equal = await cryptoFunctionService.compareFast(aByteString, bByteString);
|
||||
expect(equal).toBe(false);
|
||||
});
|
||||
@@ -224,10 +224,10 @@ describe("WebCrypto Function Service", () => {
|
||||
const a = new Uint8Array(2);
|
||||
a[0] = 1;
|
||||
a[1] = 2;
|
||||
const aByteString = Utils.fromBufferToByteString(a.buffer);
|
||||
const aByteString = Utils.fromBufferToByteString(a);
|
||||
const b = new Uint8Array(2);
|
||||
b[0] = 3;
|
||||
const bByteString = Utils.fromBufferToByteString(b.buffer);
|
||||
const bByteString = Utils.fromBufferToByteString(b);
|
||||
const equal = await cryptoFunctionService.compareFast(aByteString, bByteString);
|
||||
expect(equal).toBe(false);
|
||||
});
|
||||
@@ -239,7 +239,7 @@ describe("WebCrypto Function Service", () => {
|
||||
const iv = makeStaticByteArray(16);
|
||||
const key = makeStaticByteArray(32);
|
||||
const data = Utils.fromUtf8ToArray("EncryptMe!");
|
||||
const encValue = await cryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer);
|
||||
const encValue = await cryptoFunctionService.aesEncrypt(data, iv, key);
|
||||
expect(Utils.fromBufferToB64(encValue)).toBe("ByUF8vhyX4ddU9gcooznwA==");
|
||||
});
|
||||
|
||||
@@ -249,10 +249,10 @@ describe("WebCrypto Function Service", () => {
|
||||
const key = makeStaticByteArray(32);
|
||||
const value = "EncryptMe!";
|
||||
const data = Utils.fromUtf8ToArray(value);
|
||||
const encValue = await cryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer);
|
||||
const encValue = await cryptoFunctionService.aesEncrypt(data, iv, key);
|
||||
const encData = Utils.fromBufferToB64(encValue);
|
||||
const b64Iv = Utils.fromBufferToB64(iv.buffer);
|
||||
const symKey = new SymmetricCryptoKey(key.buffer);
|
||||
const b64Iv = Utils.fromBufferToB64(iv);
|
||||
const symKey = new SymmetricCryptoKey(key);
|
||||
const params = cryptoFunctionService.aesDecryptFastParameters(encData, b64Iv, null, symKey);
|
||||
const decValue = await cryptoFunctionService.aesDecryptFast(params);
|
||||
expect(decValue).toBe(value);
|
||||
@@ -264,8 +264,8 @@ describe("WebCrypto Function Service", () => {
|
||||
const key = makeStaticByteArray(32);
|
||||
const value = "EncryptMe!";
|
||||
const data = Utils.fromUtf8ToArray(value);
|
||||
const encValue = await cryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer);
|
||||
const decValue = await cryptoFunctionService.aesDecrypt(encValue, iv.buffer, key.buffer);
|
||||
const encValue = new Uint8Array(await cryptoFunctionService.aesEncrypt(data, iv, key));
|
||||
const decValue = await cryptoFunctionService.aesDecrypt(encValue, iv, key);
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
|
||||
});
|
||||
});
|
||||
@@ -273,8 +273,8 @@ describe("WebCrypto Function Service", () => {
|
||||
describe("aesDecryptFast", () => {
|
||||
it("should successfully decrypt data", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const iv = Utils.fromBufferToB64(makeStaticByteArray(16).buffer);
|
||||
const symKey = new SymmetricCryptoKey(makeStaticByteArray(32).buffer);
|
||||
const iv = Utils.fromBufferToB64(makeStaticByteArray(16));
|
||||
const symKey = new SymmetricCryptoKey(makeStaticByteArray(32));
|
||||
const data = "ByUF8vhyX4ddU9gcooznwA==";
|
||||
const params = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey);
|
||||
const decValue = await cryptoFunctionService.aesDecryptFast(params);
|
||||
@@ -288,7 +288,7 @@ describe("WebCrypto Function Service", () => {
|
||||
const iv = makeStaticByteArray(16);
|
||||
const key = makeStaticByteArray(32);
|
||||
const data = Utils.fromB64ToArray("ByUF8vhyX4ddU9gcooznwA==");
|
||||
const decValue = await cryptoFunctionService.aesDecrypt(data.buffer, iv.buffer, key.buffer);
|
||||
const decValue = await cryptoFunctionService.aesDecrypt(data, iv, key);
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!");
|
||||
});
|
||||
});
|
||||
@@ -300,8 +300,8 @@ describe("WebCrypto Function Service", () => {
|
||||
const privKey = Utils.fromB64ToArray(RsaPrivateKey);
|
||||
const value = "EncryptMe!";
|
||||
const data = Utils.fromUtf8ToArray(value);
|
||||
const encValue = await cryptoFunctionService.rsaEncrypt(data.buffer, pubKey.buffer, "sha1");
|
||||
const decValue = await cryptoFunctionService.rsaDecrypt(encValue, privKey.buffer, "sha1");
|
||||
const encValue = new Uint8Array(await cryptoFunctionService.rsaEncrypt(data, pubKey, "sha1"));
|
||||
const decValue = await cryptoFunctionService.rsaDecrypt(encValue, privKey, "sha1");
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
|
||||
});
|
||||
});
|
||||
@@ -316,7 +316,7 @@ describe("WebCrypto Function Service", () => {
|
||||
"zFOIEPF2S1zgperEP23M01mr4dWVdYN18B32YF67xdJHMbFhp5dkQwv9CmscoWq7OE5HIfOb+JAh7BEZb+CmKhM3yWJvoR/D" +
|
||||
"/5jcercUtK2o+XrzNrL4UQ7yLZcFz6Bfwb/j6ICYvqd/YJwXNE6dwlL57OfwJyCdw2rRYf0/qI00t9u8Iitw=="
|
||||
);
|
||||
const decValue = await cryptoFunctionService.rsaDecrypt(data.buffer, privKey.buffer, "sha1");
|
||||
const decValue = await cryptoFunctionService.rsaDecrypt(data, privKey, "sha1");
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!");
|
||||
});
|
||||
});
|
||||
@@ -325,7 +325,7 @@ describe("WebCrypto Function Service", () => {
|
||||
it("should successfully extract key", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const privKey = Utils.fromB64ToArray(RsaPrivateKey);
|
||||
const publicKey = await cryptoFunctionService.rsaExtractPublicKey(privKey.buffer);
|
||||
const publicKey = await cryptoFunctionService.rsaExtractPublicKey(privKey);
|
||||
expect(Utils.fromBufferToB64(publicKey)).toBe(RsaPublicKey);
|
||||
});
|
||||
});
|
||||
@@ -390,8 +390,8 @@ function testPbkdf2(
|
||||
it("should create valid " + algorithm + " key from array buffer input", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const key = await cryptoFunctionService.pbkdf2(
|
||||
Utils.fromUtf8ToArray(regularPassword).buffer,
|
||||
Utils.fromUtf8ToArray(regularEmail).buffer,
|
||||
Utils.fromUtf8ToArray(regularPassword),
|
||||
Utils.fromUtf8ToArray(regularEmail),
|
||||
algorithm,
|
||||
5000
|
||||
);
|
||||
@@ -437,8 +437,8 @@ function testHkdf(
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const key = await cryptoFunctionService.hkdf(
|
||||
ikm,
|
||||
Utils.fromUtf8ToArray(regularSalt).buffer,
|
||||
Utils.fromUtf8ToArray(regularInfo).buffer,
|
||||
Utils.fromUtf8ToArray(regularSalt),
|
||||
Utils.fromUtf8ToArray(regularInfo),
|
||||
32,
|
||||
algorithm
|
||||
);
|
||||
@@ -496,10 +496,7 @@ function testHash(
|
||||
|
||||
it("should create valid " + algorithm + " hash from array buffer input", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const hash = await cryptoFunctionService.hash(
|
||||
Utils.fromUtf8ToArray(regularValue).buffer,
|
||||
algorithm
|
||||
);
|
||||
const hash = await cryptoFunctionService.hash(Utils.fromUtf8ToArray(regularValue), algorithm);
|
||||
expect(Utils.fromBufferToHex(hash)).toBe(regularHash);
|
||||
});
|
||||
}
|
||||
@@ -508,8 +505,8 @@ function testHmac(algorithm: "sha1" | "sha256" | "sha512", mac: string) {
|
||||
it("should create valid " + algorithm + " hmac", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const computedMac = await cryptoFunctionService.hmac(
|
||||
Utils.fromUtf8ToArray("SignMe!!").buffer,
|
||||
Utils.fromUtf8ToArray("secretkey").buffer,
|
||||
Utils.fromUtf8ToArray("SignMe!!"),
|
||||
Utils.fromUtf8ToArray("secretkey"),
|
||||
algorithm
|
||||
);
|
||||
expect(Utils.fromBufferToHex(computedMac)).toBe(mac);
|
||||
@@ -519,14 +516,14 @@ function testHmac(algorithm: "sha1" | "sha256" | "sha512", mac: string) {
|
||||
function testHmacFast(algorithm: "sha1" | "sha256" | "sha512", mac: string) {
|
||||
it("should create valid " + algorithm + " hmac", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const keyByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray("secretkey").buffer);
|
||||
const dataByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray("SignMe!!").buffer);
|
||||
const keyByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray("secretkey"));
|
||||
const dataByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray("SignMe!!"));
|
||||
const computedMac = await cryptoFunctionService.hmacFast(
|
||||
dataByteString,
|
||||
keyByteString,
|
||||
algorithm
|
||||
);
|
||||
expect(Utils.fromBufferToHex(Utils.fromByteStringToArray(computedMac).buffer)).toBe(mac);
|
||||
expect(Utils.fromBufferToHex(Utils.fromByteStringToArray(computedMac))).toBe(mac);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -535,7 +532,9 @@ function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) {
|
||||
"should successfully generate a " + length + " bit key pair",
|
||||
async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const keyPair = await cryptoFunctionService.rsaGenerateKeyPair(length);
|
||||
const keyPair = (await cryptoFunctionService.rsaGenerateKeyPair(length)).map(
|
||||
(k) => new Uint8Array(k)
|
||||
);
|
||||
expect(keyPair[0] == null || keyPair[1] == null).toBe(false);
|
||||
const publicKey = await cryptoFunctionService.rsaExtractPublicKey(keyPair[1]);
|
||||
expect(Utils.fromBufferToB64(keyPair[0])).toBe(Utils.fromBufferToB64(publicKey));
|
||||
|
||||
@@ -20,11 +20,11 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
}
|
||||
|
||||
async pbkdf2(
|
||||
password: string | ArrayBuffer,
|
||||
salt: string | ArrayBuffer,
|
||||
password: string | Uint8Array,
|
||||
salt: string | Uint8Array,
|
||||
algorithm: "sha256" | "sha512",
|
||||
iterations: number
|
||||
): Promise<ArrayBuffer> {
|
||||
): Promise<Uint8Array> {
|
||||
const wcLen = algorithm === "sha256" ? 256 : 512;
|
||||
const passwordBuf = this.toBuf(password);
|
||||
const saltBuf = this.toBuf(salt);
|
||||
@@ -43,16 +43,17 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
false,
|
||||
["deriveBits"]
|
||||
);
|
||||
return await this.subtle.deriveBits(pbkdf2Params, impKey, wcLen);
|
||||
const buffer = await this.subtle.deriveBits(pbkdf2Params as any, impKey, wcLen);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
async argon2(
|
||||
password: string | ArrayBuffer,
|
||||
salt: string | ArrayBuffer,
|
||||
password: string | Uint8Array,
|
||||
salt: string | Uint8Array,
|
||||
iterations: number,
|
||||
memory: number,
|
||||
parallelism: number
|
||||
): Promise<ArrayBuffer> {
|
||||
): Promise<Uint8Array> {
|
||||
if (!this.wasmSupported) {
|
||||
throw "Webassembly support is required for the Argon2 KDF feature.";
|
||||
}
|
||||
@@ -69,16 +70,17 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
hashLen: 32,
|
||||
type: argon2.ArgonType.Argon2id,
|
||||
});
|
||||
argon2.unloadRuntime();
|
||||
return result.hash;
|
||||
}
|
||||
|
||||
async hkdf(
|
||||
ikm: ArrayBuffer,
|
||||
salt: string | ArrayBuffer,
|
||||
info: string | ArrayBuffer,
|
||||
ikm: Uint8Array,
|
||||
salt: string | Uint8Array,
|
||||
info: string | Uint8Array,
|
||||
outputByteSize: number,
|
||||
algorithm: "sha256" | "sha512"
|
||||
): Promise<ArrayBuffer> {
|
||||
): Promise<Uint8Array> {
|
||||
const saltBuf = this.toBuf(salt);
|
||||
const infoBuf = this.toBuf(info);
|
||||
|
||||
@@ -92,16 +94,17 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
const impKey = await this.subtle.importKey("raw", ikm, { name: "HKDF" } as any, false, [
|
||||
"deriveBits",
|
||||
]);
|
||||
return await this.subtle.deriveBits(hkdfParams as any, impKey, outputByteSize * 8);
|
||||
const buffer = await this.subtle.deriveBits(hkdfParams as any, impKey, outputByteSize * 8);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
// ref: https://tools.ietf.org/html/rfc5869
|
||||
async hkdfExpand(
|
||||
prk: ArrayBuffer,
|
||||
info: string | ArrayBuffer,
|
||||
prk: Uint8Array,
|
||||
info: string | Uint8Array,
|
||||
outputByteSize: number,
|
||||
algorithm: "sha256" | "sha512"
|
||||
): Promise<ArrayBuffer> {
|
||||
): Promise<Uint8Array> {
|
||||
const hashLen = algorithm === "sha256" ? 32 : 64;
|
||||
if (outputByteSize > 255 * hashLen) {
|
||||
throw new Error("outputByteSize is too large.");
|
||||
@@ -121,49 +124,54 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
t.set(previousT);
|
||||
t.set(infoArr, previousT.length);
|
||||
t.set([i + 1], t.length - 1);
|
||||
previousT = new Uint8Array(await this.hmac(t.buffer, prk, algorithm));
|
||||
previousT = new Uint8Array(await this.hmac(t, prk, algorithm));
|
||||
okm.set(previousT, runningOkmLength);
|
||||
runningOkmLength += previousT.length;
|
||||
if (runningOkmLength >= outputByteSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return okm.slice(0, outputByteSize).buffer;
|
||||
return okm.slice(0, outputByteSize);
|
||||
}
|
||||
|
||||
async hash(
|
||||
value: string | ArrayBuffer,
|
||||
value: string | Uint8Array,
|
||||
algorithm: "sha1" | "sha256" | "sha512" | "md5"
|
||||
): Promise<ArrayBuffer> {
|
||||
): Promise<Uint8Array> {
|
||||
if (algorithm === "md5") {
|
||||
const md = algorithm === "md5" ? forge.md.md5.create() : forge.md.sha1.create();
|
||||
const valueBytes = this.toByteString(value);
|
||||
md.update(valueBytes, "raw");
|
||||
return Utils.fromByteStringToArray(md.digest().data).buffer;
|
||||
return Utils.fromByteStringToArray(md.digest().data);
|
||||
}
|
||||
|
||||
const valueBuf = this.toBuf(value);
|
||||
return await this.subtle.digest({ name: this.toWebCryptoAlgorithm(algorithm) }, valueBuf);
|
||||
const buffer = await this.subtle.digest(
|
||||
{ name: this.toWebCryptoAlgorithm(algorithm) },
|
||||
valueBuf
|
||||
);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
async hmac(
|
||||
value: ArrayBuffer,
|
||||
key: ArrayBuffer,
|
||||
value: Uint8Array,
|
||||
key: Uint8Array,
|
||||
algorithm: "sha1" | "sha256" | "sha512"
|
||||
): Promise<ArrayBuffer> {
|
||||
): Promise<Uint8Array> {
|
||||
const signingAlgorithm = {
|
||||
name: "HMAC",
|
||||
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
|
||||
};
|
||||
|
||||
const impKey = await this.subtle.importKey("raw", key, signingAlgorithm, false, ["sign"]);
|
||||
return await this.subtle.sign(signingAlgorithm, impKey, value);
|
||||
const buffer = await this.subtle.sign(signingAlgorithm, impKey, value);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
// Safely compare two values in a way that protects against timing attacks (Double HMAC Verification).
|
||||
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
|
||||
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
|
||||
async compare(a: ArrayBuffer, b: ArrayBuffer): Promise<boolean> {
|
||||
async compare(a: Uint8Array, b: Uint8Array): Promise<boolean> {
|
||||
const macKey = await this.randomBytes(32);
|
||||
const signingAlgorithm = {
|
||||
name: "HMAC",
|
||||
@@ -218,11 +226,12 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
return equals;
|
||||
}
|
||||
|
||||
async aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
async aesEncrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
|
||||
const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [
|
||||
"encrypt",
|
||||
]);
|
||||
return await this.subtle.encrypt({ name: "AES-CBC", iv: iv }, impKey, data);
|
||||
const buffer = await this.subtle.encrypt({ name: "AES-CBC", iv: iv }, impKey, data);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
aesDecryptFastParameters(
|
||||
@@ -274,18 +283,19 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
return Promise.resolve(val);
|
||||
}
|
||||
|
||||
async aesDecrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
async aesDecrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
|
||||
const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [
|
||||
"decrypt",
|
||||
]);
|
||||
return await this.subtle.decrypt({ name: "AES-CBC", iv: iv }, impKey, data);
|
||||
const buffer = await this.subtle.decrypt({ name: "AES-CBC", iv: iv }, impKey, data);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
async rsaEncrypt(
|
||||
data: ArrayBuffer,
|
||||
publicKey: ArrayBuffer,
|
||||
data: Uint8Array,
|
||||
publicKey: Uint8Array,
|
||||
algorithm: "sha1" | "sha256"
|
||||
): Promise<ArrayBuffer> {
|
||||
): Promise<Uint8Array> {
|
||||
// Note: Edge browser requires that we specify name and hash for both key import and decrypt.
|
||||
// We cannot use the proper types here.
|
||||
const rsaParams = {
|
||||
@@ -293,14 +303,15 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
|
||||
};
|
||||
const impKey = await this.subtle.importKey("spki", publicKey, rsaParams, false, ["encrypt"]);
|
||||
return await this.subtle.encrypt(rsaParams, impKey, data);
|
||||
const buffer = await this.subtle.encrypt(rsaParams, impKey, data);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
async rsaDecrypt(
|
||||
data: ArrayBuffer,
|
||||
privateKey: ArrayBuffer,
|
||||
data: Uint8Array,
|
||||
privateKey: Uint8Array,
|
||||
algorithm: "sha1" | "sha256"
|
||||
): Promise<ArrayBuffer> {
|
||||
): Promise<Uint8Array> {
|
||||
// Note: Edge browser requires that we specify name and hash for both key import and decrypt.
|
||||
// We cannot use the proper types here.
|
||||
const rsaParams = {
|
||||
@@ -308,10 +319,11 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
|
||||
};
|
||||
const impKey = await this.subtle.importKey("pkcs8", privateKey, rsaParams, false, ["decrypt"]);
|
||||
return await this.subtle.decrypt(rsaParams, impKey, data);
|
||||
const buffer = await this.subtle.decrypt(rsaParams, impKey, data);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
async rsaExtractPublicKey(privateKey: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
async rsaExtractPublicKey(privateKey: Uint8Array): Promise<Uint8Array> {
|
||||
const rsaParams = {
|
||||
name: "RSA-OAEP",
|
||||
// Have to specify some algorithm
|
||||
@@ -331,10 +343,11 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
const impPublicKey = await this.subtle.importKey("jwk", jwkPublicKeyParams, rsaParams, true, [
|
||||
"encrypt",
|
||||
]);
|
||||
return await this.subtle.exportKey("spki", impPublicKey);
|
||||
const buffer = await this.subtle.exportKey("spki", impPublicKey);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> {
|
||||
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[Uint8Array, Uint8Array]> {
|
||||
const rsaParams = {
|
||||
name: "RSA-OAEP",
|
||||
modulusLength: length,
|
||||
@@ -348,26 +361,26 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
])) as CryptoKeyPair;
|
||||
const publicKey = await this.subtle.exportKey("spki", keyPair.publicKey);
|
||||
const privateKey = await this.subtle.exportKey("pkcs8", keyPair.privateKey);
|
||||
return [publicKey, privateKey];
|
||||
return [new Uint8Array(publicKey), new Uint8Array(privateKey)];
|
||||
}
|
||||
|
||||
randomBytes(length: number): Promise<CsprngArray> {
|
||||
const arr = new Uint8Array(length);
|
||||
this.crypto.getRandomValues(arr);
|
||||
return Promise.resolve(arr.buffer as CsprngArray);
|
||||
return Promise.resolve(arr as CsprngArray);
|
||||
}
|
||||
|
||||
private toBuf(value: string | ArrayBuffer): ArrayBuffer {
|
||||
let buf: ArrayBuffer;
|
||||
private toBuf(value: string | Uint8Array): Uint8Array {
|
||||
let buf: Uint8Array;
|
||||
if (typeof value === "string") {
|
||||
buf = Utils.fromUtf8ToArray(value).buffer;
|
||||
buf = Utils.fromUtf8ToArray(value);
|
||||
} else {
|
||||
buf = value;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
private toByteString(value: string | ArrayBuffer): string {
|
||||
private toByteString(value: string | Uint8Array): string {
|
||||
let bytes: string;
|
||||
if (typeof value === "string") {
|
||||
bytes = forge.util.encodeUtf8(value);
|
||||
|
||||
@@ -133,6 +133,7 @@ import { Utils } from "../platform/misc/utils";
|
||||
import { AttachmentRequest } from "../vault/models/request/attachment.request";
|
||||
import { CipherBulkDeleteRequest } from "../vault/models/request/cipher-bulk-delete.request";
|
||||
import { CipherBulkMoveRequest } from "../vault/models/request/cipher-bulk-move.request";
|
||||
import { CipherBulkRestoreRequest } from "../vault/models/request/cipher-bulk-restore.request";
|
||||
import { CipherBulkShareRequest } from "../vault/models/request/cipher-bulk-share.request";
|
||||
import { CipherCollectionsRequest } from "../vault/models/request/cipher-collections.request";
|
||||
import { CipherCreateRequest } from "../vault/models/request/cipher-create.request";
|
||||
@@ -614,12 +615,19 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
}
|
||||
|
||||
async putRestoreManyCiphers(
|
||||
request: CipherBulkDeleteRequest
|
||||
request: CipherBulkRestoreRequest
|
||||
): Promise<ListResponse<CipherResponse>> {
|
||||
const r = await this.send("PUT", "/ciphers/restore", request, true, true);
|
||||
return new ListResponse<CipherResponse>(r, CipherResponse);
|
||||
}
|
||||
|
||||
async putRestoreManyCiphersAdmin(
|
||||
request: CipherBulkRestoreRequest
|
||||
): Promise<ListResponse<CipherResponse>> {
|
||||
const r = await this.send("PUT", "/ciphers/restore-admin", request, true, true);
|
||||
return new ListResponse<CipherResponse>(r, CipherResponse);
|
||||
}
|
||||
|
||||
// Attachments APIs
|
||||
|
||||
async getAttachmentData(
|
||||
@@ -881,7 +889,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
// Plan APIs
|
||||
|
||||
async getPlans(): Promise<ListResponse<PlanResponse>> {
|
||||
const r = await this.send("GET", "/plans/", null, false, true);
|
||||
const r = await this.send("GET", "/plans/all", null, false, true);
|
||||
return new ListResponse(r, PlanResponse);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,10 +57,10 @@ describe("deviceCryptoService", () => {
|
||||
let makeDeviceKeySpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
mockRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
|
||||
mockDeviceKey = new SymmetricCryptoKey(mockRandomBytes);
|
||||
existingDeviceKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray
|
||||
new Uint8Array(deviceKeyBytesLength) as CsprngArray
|
||||
) as DeviceKey;
|
||||
|
||||
stateSvcGetDeviceKeySpy = jest.spyOn(stateService, "getDeviceKey");
|
||||
@@ -97,7 +97,7 @@ describe("deviceCryptoService", () => {
|
||||
|
||||
describe("makeDeviceKey", () => {
|
||||
it("creates a new non-null 64 byte device key, securely stores it, and returns it", async () => {
|
||||
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
|
||||
|
||||
const cryptoFuncSvcRandomBytesSpy = jest
|
||||
.spyOn(cryptoFunctionService, "randomBytes")
|
||||
@@ -128,9 +128,9 @@ describe("deviceCryptoService", () => {
|
||||
let mockUserSymKey: SymmetricCryptoKey;
|
||||
|
||||
const deviceRsaKeyLength = 2048;
|
||||
let mockDeviceRsaKeyPair: [ArrayBuffer, ArrayBuffer];
|
||||
let mockDevicePrivateKey: ArrayBuffer;
|
||||
let mockDevicePublicKey: ArrayBuffer;
|
||||
let mockDeviceRsaKeyPair: [Uint8Array, Uint8Array];
|
||||
let mockDevicePrivateKey: Uint8Array;
|
||||
let mockDevicePublicKey: Uint8Array;
|
||||
let mockDevicePublicKeyEncryptedUserSymKey: EncString;
|
||||
let mockUserSymKeyEncryptedDevicePublicKey: EncString;
|
||||
let mockDeviceKeyEncryptedDevicePrivateKey: EncString;
|
||||
@@ -156,15 +156,15 @@ describe("deviceCryptoService", () => {
|
||||
beforeEach(() => {
|
||||
// Setup all spies and default return values for the happy path
|
||||
|
||||
mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
|
||||
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
|
||||
|
||||
mockUserSymKeyRandomBytes = new Uint8Array(userSymKeyBytesLength).buffer as CsprngArray;
|
||||
mockUserSymKeyRandomBytes = new Uint8Array(userSymKeyBytesLength) as CsprngArray;
|
||||
mockUserSymKey = new SymmetricCryptoKey(mockUserSymKeyRandomBytes);
|
||||
|
||||
mockDeviceRsaKeyPair = [
|
||||
new ArrayBuffer(deviceRsaKeyLength),
|
||||
new ArrayBuffer(deviceRsaKeyLength),
|
||||
new Uint8Array(deviceRsaKeyLength),
|
||||
new Uint8Array(deviceRsaKeyLength),
|
||||
];
|
||||
|
||||
mockDevicePublicKey = mockDeviceRsaKeyPair[0];
|
||||
|
||||
@@ -206,6 +206,19 @@ export class OrganizationUserServiceImplementation implements OrganizationUserSe
|
||||
return new ListResponse(r, OrganizationUserBulkResponse);
|
||||
}
|
||||
|
||||
async putOrganizationUserBulkEnableSecretsManager(
|
||||
organizationId: string,
|
||||
ids: string[]
|
||||
): Promise<void> {
|
||||
await this.apiService.send(
|
||||
"PUT",
|
||||
"/organizations/" + organizationId + "/users/enable-secrets-manager",
|
||||
new OrganizationUserBulkRequest(ids),
|
||||
true,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
putOrganizationUser(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
|
||||
@@ -162,7 +162,7 @@ export class TotpService implements TotpServiceAbstraction {
|
||||
timeBytes: Uint8Array,
|
||||
alg: "sha1" | "sha256" | "sha512"
|
||||
) {
|
||||
const signature = await this.cryptoFunctionService.hmac(timeBytes.buffer, keyBytes.buffer, alg);
|
||||
const signature = await this.cryptoFunctionService.hmac(timeBytes, keyBytes, alg);
|
||||
return new Uint8Array(signature);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export class SendView implements View {
|
||||
accessId: string = null;
|
||||
name: string = null;
|
||||
notes: string = null;
|
||||
key: ArrayBuffer;
|
||||
key: Uint8Array;
|
||||
cryptoKey: SymmetricCryptoKey;
|
||||
type: SendType = null;
|
||||
text = new SendTextView();
|
||||
@@ -82,7 +82,7 @@ export class SendView implements View {
|
||||
}
|
||||
|
||||
return Object.assign(new SendView(), json, {
|
||||
key: Utils.fromB64ToArray(json.key)?.buffer,
|
||||
key: Utils.fromB64ToArray(json.key),
|
||||
cryptoKey: SymmetricCryptoKey.fromJSON(json.cryptoKey),
|
||||
text: SendTextView.fromJSON(json.text),
|
||||
file: SendFileView.fromJSON(json.file),
|
||||
|
||||
@@ -241,7 +241,7 @@ export class SendService implements InternalSendServiceAbstraction {
|
||||
key: SymmetricCryptoKey
|
||||
): Promise<[EncString, EncArrayBuffer]> {
|
||||
const encFileName = await this.cryptoService.encrypt(fileName, key);
|
||||
const encFileData = await this.cryptoService.encryptToBytes(data, key);
|
||||
const encFileData = await this.cryptoService.encryptToBytes(new Uint8Array(data), key);
|
||||
return [encFileName, encFileData];
|
||||
}
|
||||
|
||||
|
||||
2
libs/common/src/types/csprng.d.ts
vendored
2
libs/common/src/types/csprng.d.ts
vendored
@@ -4,6 +4,6 @@ import { Opaque } from "type-fest";
|
||||
// represents an array or string value generated from a
|
||||
// cryptographic secure pseudorandom number generator (CSPRNG)
|
||||
|
||||
type CsprngArray = Opaque<ArrayBuffer, "CSPRNG">;
|
||||
type CsprngArray = Opaque<Uint8Array, "CSPRNG">;
|
||||
|
||||
type CsprngString = Opaque<string, "CSPRNG">;
|
||||
|
||||
@@ -33,8 +33,8 @@ export abstract class CipherService {
|
||||
updateLastUsedDate: (id: string) => Promise<void>;
|
||||
updateLastLaunchedDate: (id: string) => Promise<void>;
|
||||
saveNeverDomain: (domain: string) => Promise<void>;
|
||||
createWithServer: (cipher: Cipher) => Promise<Cipher>;
|
||||
updateWithServer: (cipher: Cipher) => Promise<any>;
|
||||
createWithServer: (cipher: Cipher, orgAdmin?: boolean) => Promise<any>;
|
||||
updateWithServer: (cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean) => Promise<any>;
|
||||
shareWithServer: (
|
||||
cipher: CipherView,
|
||||
organizationId: string,
|
||||
@@ -76,5 +76,9 @@ export abstract class CipherService {
|
||||
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[]
|
||||
) => Promise<any>;
|
||||
restoreWithServer: (id: string, asAdmin?: boolean) => Promise<any>;
|
||||
restoreManyWithServer: (ids: string[]) => Promise<any>;
|
||||
restoreManyWithServer: (
|
||||
ids: string[],
|
||||
organizationId?: string,
|
||||
asAdmin?: boolean
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user