mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-09 19:28:06 +01:00
Merge branch 'main' into auth/pm-8111/browser-refresh-login-component
This commit is contained in:
commit
c0d37bcc62
45
.github/workflows/build-desktop.yml
vendored
45
.github/workflows/build-desktop.yml
vendored
@ -174,20 +174,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
apps/desktop/desktop_native/napi/*.node
|
apps/desktop/desktop_native/napi/*.node
|
||||||
|
apps/desktop/desktop_native/dist/*
|
||||||
${{ env.RUNNER_TEMP }}/.cargo/registry
|
${{ env.RUNNER_TEMP }}/.cargo/registry
|
||||||
${{ env.RUNNER_TEMP }}/.cargo/git
|
${{ env.RUNNER_TEMP }}/.cargo/git
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||||
|
|
||||||
- name: Build Native Module
|
- name: Build Native Module
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
working-directory: apps/desktop/desktop_native/napi
|
working-directory: apps/desktop/desktop_native
|
||||||
env:
|
env:
|
||||||
PKG_CONFIG_ALLOW_CROSS: true
|
PKG_CONFIG_ALLOW_CROSS: true
|
||||||
PKG_CONFIG_ALL_STATIC: true
|
PKG_CONFIG_ALL_STATIC: true
|
||||||
TARGET: musl
|
TARGET: musl
|
||||||
run: |
|
run: |
|
||||||
rustup target add x86_64-unknown-linux-musl
|
rustup target add x86_64-unknown-linux-musl
|
||||||
npm run build:cross-platform
|
node build.js cross-platform
|
||||||
|
|
||||||
- name: Build application
|
- name: Build application
|
||||||
run: npm run dist:lin
|
run: npm run dist:lin
|
||||||
@ -301,13 +302,15 @@ jobs:
|
|||||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: apps/desktop/desktop_native/napi/*.node
|
path: |
|
||||||
|
apps/desktop/desktop_native/napi/*.node
|
||||||
|
apps/desktop/desktop_native/dist/*
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||||
|
|
||||||
- name: Build Native Module
|
- name: Build Native Module
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
working-directory: apps/desktop/desktop_native/napi
|
working-directory: apps/desktop/desktop_native
|
||||||
run: npm run build:cross-platform
|
run: node build.js cross-platform
|
||||||
|
|
||||||
- name: Build & Sign (dev)
|
- name: Build & Sign (dev)
|
||||||
env:
|
env:
|
||||||
@ -584,13 +587,15 @@ jobs:
|
|||||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: apps/desktop/desktop_native/napi/*.node
|
path: |
|
||||||
|
apps/desktop/desktop_native/napi/*.node
|
||||||
|
apps/desktop/desktop_native/dist/*
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||||
|
|
||||||
- name: Build Native Module
|
- name: Build Native Module
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
working-directory: apps/desktop/desktop_native/napi
|
working-directory: apps/desktop/desktop_native
|
||||||
run: npm run build:cross-platform
|
run: node build.js cross-platform
|
||||||
|
|
||||||
- name: Build application (dev)
|
- name: Build application (dev)
|
||||||
run: npm run build
|
run: npm run build
|
||||||
@ -748,13 +753,15 @@ jobs:
|
|||||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: apps/desktop/desktop_native/napi/*.node
|
path: |
|
||||||
|
apps/desktop/desktop_native/napi/*.node
|
||||||
|
apps/desktop/desktop_native/dist/*
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||||
|
|
||||||
- name: Build Native Module
|
- name: Build Native Module
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
working-directory: apps/desktop/desktop_native/napi
|
working-directory: apps/desktop/desktop_native
|
||||||
run: npm run build:cross-platform
|
run: node build.js cross-platform
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||||
@ -972,13 +979,15 @@ jobs:
|
|||||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: apps/desktop/desktop_native/napi/*.node
|
path: |
|
||||||
|
apps/desktop/desktop_native/napi/*.node
|
||||||
|
apps/desktop/desktop_native/dist/*
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||||
|
|
||||||
- name: Build Native Module
|
- name: Build Native Module
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
working-directory: apps/desktop/desktop_native/napi
|
working-directory: apps/desktop/desktop_native
|
||||||
run: npm run build:cross-platform
|
run: node build.js cross-platform
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||||
@ -1205,13 +1214,15 @@ jobs:
|
|||||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: apps/desktop/desktop_native/napi/*.node
|
path: |
|
||||||
|
apps/desktop/desktop_native/napi/*.node
|
||||||
|
apps/desktop/desktop_native/dist/*
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||||
|
|
||||||
- name: Build Native Module
|
- name: Build Native Module
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
working-directory: apps/desktop/desktop_native/napi
|
working-directory: apps/desktop/desktop_native
|
||||||
run: npm run build:cross-platform
|
run: node build.js cross-platform
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||||
|
@ -619,6 +619,15 @@
|
|||||||
"yourVaultIsLocked": {
|
"yourVaultIsLocked": {
|
||||||
"message": "Your vault is locked. Verify your identity to continue."
|
"message": "Your vault is locked. Verify your identity to continue."
|
||||||
},
|
},
|
||||||
|
"yourVaultIsLockedV2": {
|
||||||
|
"message": "Your vault is locked"
|
||||||
|
},
|
||||||
|
"yourAccountIsLocked": {
|
||||||
|
"message": "Your account is locked"
|
||||||
|
},
|
||||||
|
"or": {
|
||||||
|
"message": "or"
|
||||||
|
},
|
||||||
"unlock": {
|
"unlock": {
|
||||||
"message": "Unlock"
|
"message": "Unlock"
|
||||||
},
|
},
|
||||||
@ -1954,6 +1963,9 @@
|
|||||||
"unlockWithBiometrics": {
|
"unlockWithBiometrics": {
|
||||||
"message": "Unlock with biometrics"
|
"message": "Unlock with biometrics"
|
||||||
},
|
},
|
||||||
|
"unlockWithMasterPassword": {
|
||||||
|
"message": "Unlock with master password"
|
||||||
|
},
|
||||||
"awaitDesktop": {
|
"awaitDesktop": {
|
||||||
"message": "Awaiting confirmation from desktop"
|
"message": "Awaiting confirmation from desktop"
|
||||||
},
|
},
|
||||||
@ -3641,6 +3653,9 @@
|
|||||||
"typePasskey": {
|
"typePasskey": {
|
||||||
"message": "Passkey"
|
"message": "Passkey"
|
||||||
},
|
},
|
||||||
|
"accessing": {
|
||||||
|
"message": "Accessing"
|
||||||
|
},
|
||||||
"passkeyNotCopied": {
|
"passkeyNotCopied": {
|
||||||
"message": "Passkey will not be copied"
|
"message": "Passkey will not be copied"
|
||||||
},
|
},
|
||||||
|
@ -59,7 +59,7 @@ export class CurrentAccountComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async currentAccountClicked() {
|
async currentAccountClicked() {
|
||||||
if (this.route.snapshot.data.state.includes("account-switcher")) {
|
if (this.route.snapshot.data?.state?.includes("account-switcher")) {
|
||||||
this.location.back();
|
this.location.back();
|
||||||
} else {
|
} else {
|
||||||
await this.router.navigate(["/account-switcher"]);
|
await this.router.navigate(["/account-switcher"]);
|
||||||
|
@ -57,19 +57,4 @@ describe("FIDO2 page-script for manifest v2", () => {
|
|||||||
);
|
);
|
||||||
expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`);
|
expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes the appended `page-script.js` file after the script has triggered a load event", () => {
|
|
||||||
createdScriptElement = document.createElement("script");
|
|
||||||
jest.spyOn(window.document, "createElement").mockImplementation((element) => {
|
|
||||||
return createdScriptElement;
|
|
||||||
});
|
|
||||||
|
|
||||||
require("./fido2-page-script-append.mv2");
|
|
||||||
|
|
||||||
jest.spyOn(createdScriptElement, "remove");
|
|
||||||
createdScriptElement.dispatchEvent(new Event("load"));
|
|
||||||
jest.runAllTimers();
|
|
||||||
|
|
||||||
expect(createdScriptElement.remove).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -9,13 +9,8 @@
|
|||||||
|
|
||||||
const script = globalContext.document.createElement("script");
|
const script = globalContext.document.createElement("script");
|
||||||
script.src = chrome.runtime.getURL("content/fido2-page-script.js");
|
script.src = chrome.runtime.getURL("content/fido2-page-script.js");
|
||||||
script.addEventListener("load", removeScriptOnLoad);
|
|
||||||
|
|
||||||
const scriptInsertionPoint =
|
const scriptInsertionPoint =
|
||||||
globalContext.document.head || globalContext.document.documentElement;
|
globalContext.document.head || globalContext.document.documentElement;
|
||||||
scriptInsertionPoint.prepend(script);
|
scriptInsertionPoint.prepend(script);
|
||||||
|
|
||||||
function removeScriptOnLoad() {
|
|
||||||
globalThis.setTimeout(() => script?.remove(), 5000);
|
|
||||||
}
|
|
||||||
})(globalThis);
|
})(globalThis);
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
const script = globalContext.document.createElement("script");
|
const script = globalContext.document.createElement("script");
|
||||||
script.src = chrome.runtime.getURL("content/fido2-page-script.js");
|
script.src = chrome.runtime.getURL("content/fido2-page-script.js");
|
||||||
script.addEventListener("load", removeScriptOnLoad);
|
|
||||||
|
|
||||||
// We are ensuring that the script injection is delayed in the event that we are loading
|
// We are ensuring that the script injection is delayed in the event that we are loading
|
||||||
// within an iframe element. This prevents an issue with web mail clients that load content
|
// within an iframe element. This prevents an issue with web mail clients that load content
|
||||||
@ -29,8 +28,4 @@
|
|||||||
globalContext.document.head || globalContext.document.documentElement;
|
globalContext.document.head || globalContext.document.documentElement;
|
||||||
scriptInsertionPoint.prepend(script);
|
scriptInsertionPoint.prepend(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeScriptOnLoad() {
|
|
||||||
globalThis.setTimeout(() => script?.remove(), 5000);
|
|
||||||
}
|
|
||||||
})(globalThis);
|
})(globalThis);
|
||||||
|
@ -753,6 +753,7 @@ export default class MainBackground {
|
|||||||
this.accountService,
|
this.accountService,
|
||||||
this.masterPasswordService,
|
this.masterPasswordService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
|
this.encryptService,
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
@ -43,17 +43,23 @@ function buildRegisterContentScriptsPolyfill() {
|
|||||||
function NestedProxy<T extends object>(target: T): T {
|
function NestedProxy<T extends object>(target: T): T {
|
||||||
return new Proxy(target, {
|
return new Proxy(target, {
|
||||||
get(target, prop) {
|
get(target, prop) {
|
||||||
if (!target[prop as keyof T]) {
|
const propertyValue = target[prop as keyof T];
|
||||||
|
|
||||||
|
if (!propertyValue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof target[prop as keyof T] !== "function") {
|
if (typeof propertyValue === "object") {
|
||||||
return NestedProxy(target[prop as keyof T]);
|
return NestedProxy<typeof propertyValue>(propertyValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof propertyValue !== "function") {
|
||||||
|
return propertyValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (...arguments_: any[]) =>
|
return (...arguments_: any[]) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
target[prop as keyof T](...arguments_, (result: any) => {
|
propertyValue(...arguments_, (result: any) => {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
reject(new Error(chrome.runtime.lastError.message));
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
} else {
|
} else {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<div class="tw-flex tw-flex-1">
|
<div class="tw-flex tw-flex-1">
|
||||||
<a
|
<a
|
||||||
*ngFor="let button of navButtons"
|
*ngFor="let button of navButtons"
|
||||||
class="tw-group tw-flex tw-flex-col tw-items-center tw-gap-1 tw-px-0.5 tw-pb-2 tw-pt-3 tw-w-1/4 tw-no-underline hover:tw-no-underline hover:tw-text-primary-600 hover:tw-bg-primary-100 tw-border-2 tw-border-solid tw-border-transparent focus-visible:tw-rounded-lg focus-visible:tw-border-primary-500"
|
class="tw-flex-1 tw-group tw-flex tw-flex-col tw-items-center tw-gap-1 tw-px-0.5 tw-pb-2 tw-pt-3 tw-no-underline hover:tw-no-underline hover:tw-text-primary-600 hover:tw-bg-primary-100 tw-border-2 tw-border-solid tw-border-transparent focus-visible:tw-rounded-lg focus-visible:tw-border-primary-500"
|
||||||
[ngClass]="rla.isActive ? 'tw-font-bold tw-text-primary-600' : 'tw-text-muted'"
|
[ngClass]="rla.isActive ? 'tw-font-bold tw-text-primary-600' : 'tw-text-muted'"
|
||||||
[title]="button.label"
|
[title]="button.label"
|
||||||
[routerLink]="button.page"
|
[routerLink]="button.page"
|
||||||
|
@ -20,6 +20,8 @@ import {
|
|||||||
AnonLayoutWrapperData,
|
AnonLayoutWrapperData,
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
LoginSecondaryContentComponent,
|
LoginSecondaryContentComponent,
|
||||||
|
LockIcon,
|
||||||
|
LockV2Component,
|
||||||
PasswordHintComponent,
|
PasswordHintComponent,
|
||||||
RegistrationFinishComponent,
|
RegistrationFinishComponent,
|
||||||
RegistrationStartComponent,
|
RegistrationStartComponent,
|
||||||
@ -179,6 +181,7 @@ const routes: Routes = [
|
|||||||
path: "lock",
|
path: "lock",
|
||||||
component: LockComponent,
|
component: LockComponent,
|
||||||
canActivate: [lockGuard()],
|
canActivate: [lockGuard()],
|
||||||
|
canMatch: [extensionRefreshRedirect("/lockV2")],
|
||||||
data: { state: "lock", doNotSaveUrl: true } satisfies RouteDataProperties,
|
data: { state: "lock", doNotSaveUrl: true } satisfies RouteDataProperties,
|
||||||
},
|
},
|
||||||
...twofactorRefactorSwap(
|
...twofactorRefactorSwap(
|
||||||
@ -465,6 +468,28 @@ const routes: Routes = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: ExtensionAnonLayoutWrapperComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "lockV2",
|
||||||
|
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()],
|
||||||
|
data: {
|
||||||
|
pageIcon: LockIcon,
|
||||||
|
pageTitle: "yourVaultIsLockedV2",
|
||||||
|
showReadonlyHostname: true,
|
||||||
|
showAcctSwitcher: true,
|
||||||
|
} satisfies ExtensionAnonLayoutWrapperData,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: LockV2Component,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
component: AnonLayoutWrapperComponent,
|
component: AnonLayoutWrapperComponent,
|
||||||
|
@ -16,7 +16,11 @@ import {
|
|||||||
CLIENT_TYPE,
|
CLIENT_TYPE,
|
||||||
} from "@bitwarden/angular/services/injection-tokens";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { AnonLayoutWrapperDataService, LoginComponentService } from "@bitwarden/auth/angular";
|
import {
|
||||||
|
AnonLayoutWrapperDataService,
|
||||||
|
LoginComponentService,
|
||||||
|
LockComponentService,
|
||||||
|
} from "@bitwarden/auth/angular";
|
||||||
import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common";
|
import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||||
@ -129,6 +133,7 @@ import { ForegroundTaskSchedulerService } from "../../platform/services/task-sch
|
|||||||
import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider";
|
import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider";
|
||||||
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
|
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
|
||||||
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
|
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
|
||||||
|
import { ExtensionLockComponentService } from "../../services/extension-lock-component.service";
|
||||||
import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service";
|
import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service";
|
||||||
import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service";
|
import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service";
|
||||||
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
|
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
|
||||||
@ -548,6 +553,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
provide: CLIENT_TYPE,
|
provide: CLIENT_TYPE,
|
||||||
useValue: ClientType.Browser,
|
useValue: ClientType.Browser,
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: LockComponentService,
|
||||||
|
useClass: ExtensionLockComponentService,
|
||||||
|
deps: [],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: Fido2UserVerificationService,
|
provide: Fido2UserVerificationService,
|
||||||
useClass: Fido2UserVerificationService,
|
useClass: Fido2UserVerificationService,
|
||||||
|
@ -0,0 +1,325 @@
|
|||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
|
import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/auth/angular";
|
||||||
|
import {
|
||||||
|
PinServiceAbstraction,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
} from "@bitwarden/auth/common";
|
||||||
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { BiometricsService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { BrowserRouterService } from "../platform/popup/services/browser-router.service";
|
||||||
|
|
||||||
|
import { ExtensionLockComponentService } from "./extension-lock-component.service";
|
||||||
|
|
||||||
|
describe("ExtensionLockComponentService", () => {
|
||||||
|
let service: ExtensionLockComponentService;
|
||||||
|
|
||||||
|
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||||
|
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||||
|
let biometricsService: MockProxy<BiometricsService>;
|
||||||
|
let pinService: MockProxy<PinServiceAbstraction>;
|
||||||
|
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||||
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
|
let routerService: MockProxy<BrowserRouterService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||||
|
platformUtilsService = mock<PlatformUtilsService>();
|
||||||
|
biometricsService = mock<BiometricsService>();
|
||||||
|
pinService = mock<PinServiceAbstraction>();
|
||||||
|
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||||
|
cryptoService = mock<CryptoService>();
|
||||||
|
routerService = mock<BrowserRouterService>();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
ExtensionLockComponentService,
|
||||||
|
{
|
||||||
|
provide: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
useValue: userDecryptionOptionsService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PlatformUtilsService,
|
||||||
|
useValue: platformUtilsService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: BiometricsService,
|
||||||
|
useValue: biometricsService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PinServiceAbstraction,
|
||||||
|
useValue: pinService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: VaultTimeoutSettingsService,
|
||||||
|
useValue: vaultTimeoutSettingsService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: CryptoService,
|
||||||
|
useValue: cryptoService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: BrowserRouterService,
|
||||||
|
useValue: routerService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(ExtensionLockComponentService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("instantiates", () => {
|
||||||
|
expect(service).not.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPreviousUrl", () => {
|
||||||
|
it("returns the previous URL", () => {
|
||||||
|
routerService.getPreviousUrl.mockReturnValue("previousUrl");
|
||||||
|
expect(service.getPreviousUrl()).toBe("previousUrl");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getBiometricsError", () => {
|
||||||
|
it("returns a biometric error description when given a valid error type", () => {
|
||||||
|
expect(
|
||||||
|
service.getBiometricsError({
|
||||||
|
message: "startDesktop",
|
||||||
|
}),
|
||||||
|
).toBe("startDesktopDesc");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when given an invalid error type", () => {
|
||||||
|
expect(
|
||||||
|
service.getBiometricsError({
|
||||||
|
message: "invalidError",
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when given a null input", () => {
|
||||||
|
expect(service.getBiometricsError(null)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isWindowVisible", () => {
|
||||||
|
it("throws an error", async () => {
|
||||||
|
await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getBiometricsUnlockBtnText", () => {
|
||||||
|
it("returns the biometric unlock button text", () => {
|
||||||
|
expect(service.getBiometricsUnlockBtnText()).toBe("unlockWithBiometrics");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAvailableUnlockOptions$", () => {
|
||||||
|
interface MockInputs {
|
||||||
|
hasMasterPassword: boolean;
|
||||||
|
osSupportsBiometric: boolean;
|
||||||
|
biometricLockSet: boolean;
|
||||||
|
hasBiometricEncryptedUserKeyStored: boolean;
|
||||||
|
platformSupportsSecureStorage: boolean;
|
||||||
|
pinDecryptionAvailable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const table: [MockInputs, UnlockOptions][] = [
|
||||||
|
[
|
||||||
|
// MP + PIN + Biometrics available
|
||||||
|
{
|
||||||
|
hasMasterPassword: true,
|
||||||
|
osSupportsBiometric: true,
|
||||||
|
biometricLockSet: true,
|
||||||
|
hasBiometricEncryptedUserKeyStored: true,
|
||||||
|
platformSupportsSecureStorage: true,
|
||||||
|
pinDecryptionAvailable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: true,
|
||||||
|
disableReason: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// PIN + Biometrics available
|
||||||
|
{
|
||||||
|
hasMasterPassword: false,
|
||||||
|
osSupportsBiometric: true,
|
||||||
|
biometricLockSet: true,
|
||||||
|
hasBiometricEncryptedUserKeyStored: true,
|
||||||
|
platformSupportsSecureStorage: true,
|
||||||
|
pinDecryptionAvailable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: true,
|
||||||
|
disableReason: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// Biometrics available: user key stored with no secure storage
|
||||||
|
{
|
||||||
|
hasMasterPassword: false,
|
||||||
|
osSupportsBiometric: true,
|
||||||
|
biometricLockSet: true,
|
||||||
|
hasBiometricEncryptedUserKeyStored: true,
|
||||||
|
platformSupportsSecureStorage: false,
|
||||||
|
pinDecryptionAvailable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: true,
|
||||||
|
disableReason: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// Biometrics available: no user key stored with no secure storage
|
||||||
|
{
|
||||||
|
hasMasterPassword: false,
|
||||||
|
osSupportsBiometric: true,
|
||||||
|
biometricLockSet: true,
|
||||||
|
hasBiometricEncryptedUserKeyStored: false,
|
||||||
|
platformSupportsSecureStorage: false,
|
||||||
|
pinDecryptionAvailable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: true,
|
||||||
|
disableReason: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// Biometrics not available: biometric lock not set
|
||||||
|
{
|
||||||
|
hasMasterPassword: false,
|
||||||
|
osSupportsBiometric: true,
|
||||||
|
biometricLockSet: false,
|
||||||
|
hasBiometricEncryptedUserKeyStored: true,
|
||||||
|
platformSupportsSecureStorage: true,
|
||||||
|
pinDecryptionAvailable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: false,
|
||||||
|
disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// Biometrics not available: user key not stored
|
||||||
|
{
|
||||||
|
hasMasterPassword: false,
|
||||||
|
osSupportsBiometric: true,
|
||||||
|
biometricLockSet: true,
|
||||||
|
hasBiometricEncryptedUserKeyStored: false,
|
||||||
|
platformSupportsSecureStorage: true,
|
||||||
|
pinDecryptionAvailable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: false,
|
||||||
|
disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// Biometrics not available: OS doesn't support
|
||||||
|
{
|
||||||
|
hasMasterPassword: false,
|
||||||
|
osSupportsBiometric: false,
|
||||||
|
biometricLockSet: true,
|
||||||
|
hasBiometricEncryptedUserKeyStored: true,
|
||||||
|
platformSupportsSecureStorage: true,
|
||||||
|
pinDecryptionAvailable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: false,
|
||||||
|
disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(table)("returns unlock options", async (mockInputs, expectedOutput) => {
|
||||||
|
const userId = "userId" as UserId;
|
||||||
|
const userDecryptionOptions = {
|
||||||
|
hasMasterPassword: mockInputs.hasMasterPassword,
|
||||||
|
};
|
||||||
|
|
||||||
|
// MP
|
||||||
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
|
of(userDecryptionOptions),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Biometrics
|
||||||
|
biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric);
|
||||||
|
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet);
|
||||||
|
cryptoService.hasUserKeyStored.mockResolvedValue(
|
||||||
|
mockInputs.hasBiometricEncryptedUserKeyStored,
|
||||||
|
);
|
||||||
|
platformUtilsService.supportsSecureStorage.mockReturnValue(
|
||||||
|
mockInputs.platformSupportsSecureStorage,
|
||||||
|
);
|
||||||
|
|
||||||
|
// PIN
|
||||||
|
pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable);
|
||||||
|
|
||||||
|
const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId));
|
||||||
|
|
||||||
|
expect(unlockOptions).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
117
apps/browser/src/services/extension-lock-component.service.ts
Normal file
117
apps/browser/src/services/extension-lock-component.service.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { inject } from "@angular/core";
|
||||||
|
import { combineLatest, defer, map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BiometricsDisableReason,
|
||||||
|
LockComponentService,
|
||||||
|
UnlockOptions,
|
||||||
|
} from "@bitwarden/auth/angular";
|
||||||
|
import {
|
||||||
|
PinServiceAbstraction,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
} from "@bitwarden/auth/common";
|
||||||
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { BiometricsService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { BiometricErrors, BiometricErrorTypes } from "../models/biometricErrors";
|
||||||
|
import { BrowserRouterService } from "../platform/popup/services/browser-router.service";
|
||||||
|
|
||||||
|
export class ExtensionLockComponentService implements LockComponentService {
|
||||||
|
private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction);
|
||||||
|
private readonly platformUtilsService = inject(PlatformUtilsService);
|
||||||
|
private readonly biometricsService = inject(BiometricsService);
|
||||||
|
private readonly pinService = inject(PinServiceAbstraction);
|
||||||
|
private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||||
|
private readonly cryptoService = inject(CryptoService);
|
||||||
|
private readonly routerService = inject(BrowserRouterService);
|
||||||
|
|
||||||
|
getPreviousUrl(): string | null {
|
||||||
|
return this.routerService.getPreviousUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBiometricsError(error: any): string | null {
|
||||||
|
const biometricsError = BiometricErrors[error?.message as BiometricErrorTypes];
|
||||||
|
|
||||||
|
if (!biometricsError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return biometricsError.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isWindowVisible(): Promise<boolean> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
getBiometricsUnlockBtnText(): string {
|
||||||
|
return "unlockWithBiometrics";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isBiometricLockSet(userId: UserId): Promise<boolean> {
|
||||||
|
const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId);
|
||||||
|
const hasBiometricEncryptedUserKeyStored = await this.cryptoService.hasUserKeyStored(
|
||||||
|
KeySuffixOptions.Biometric,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBiometricsDisabledReason(
|
||||||
|
osSupportsBiometric: boolean,
|
||||||
|
biometricLockSet: boolean,
|
||||||
|
): BiometricsDisableReason | null {
|
||||||
|
if (!osSupportsBiometric) {
|
||||||
|
return BiometricsDisableReason.NotSupportedOnOperatingSystem;
|
||||||
|
} else if (!biometricLockSet) {
|
||||||
|
return BiometricsDisableReason.EncryptedKeysUnavailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions> {
|
||||||
|
return combineLatest([
|
||||||
|
// Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to
|
||||||
|
defer(() => this.biometricsService.supportsBiometric()),
|
||||||
|
defer(() => this.isBiometricLockSet(userId)),
|
||||||
|
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||||
|
defer(() => this.pinService.isPinDecryptionAvailable(userId)),
|
||||||
|
]).pipe(
|
||||||
|
map(
|
||||||
|
([
|
||||||
|
supportsBiometric,
|
||||||
|
isBiometricsLockSet,
|
||||||
|
userDecryptionOptions,
|
||||||
|
pinDecryptionAvailable,
|
||||||
|
]) => {
|
||||||
|
const disableReason = this.getBiometricsDisabledReason(
|
||||||
|
supportsBiometric,
|
||||||
|
isBiometricsLockSet,
|
||||||
|
);
|
||||||
|
|
||||||
|
const unlockOpts: UnlockOptions = {
|
||||||
|
masterPassword: {
|
||||||
|
enabled: userDecryptionOptions.hasMasterPassword,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: pinDecryptionAvailable,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: supportsBiometric && isBiometricsLockSet,
|
||||||
|
disableReason: disableReason,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return unlockOpts;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,8 @@
|
|||||||
<tools-send-form
|
<tools-send-form
|
||||||
formId="sendForm"
|
formId="sendForm"
|
||||||
[config]="config"
|
[config]="config"
|
||||||
(sendSaved)="onSendSaved()"
|
(onSendCreated)="onSendCreated($event)"
|
||||||
|
(onSendUpdated)="onSendUpdated($event)"
|
||||||
[submitBtn]="submitBtn"
|
[submitBtn]="submitBtn"
|
||||||
>
|
>
|
||||||
</tools-send-form>
|
</tools-send-form>
|
||||||
|
@ -2,12 +2,13 @@ import { CommonModule, Location } from "@angular/common";
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { FormsModule } from "@angular/forms";
|
import { FormsModule } from "@angular/forms";
|
||||||
import { ActivatedRoute, Params } from "@angular/router";
|
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||||
import { map, switchMap } from "rxjs";
|
import { map, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||||
|
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||||
import { SendId } from "@bitwarden/common/types/guid";
|
import { SendId } from "@bitwarden/common/types/guid";
|
||||||
import {
|
import {
|
||||||
@ -95,14 +96,25 @@ export class SendAddEditComponent {
|
|||||||
private sendApiService: SendApiService,
|
private sendApiService: SendApiService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
|
private router: Router,
|
||||||
) {
|
) {
|
||||||
this.subscribeToParams();
|
this.subscribeToParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the event when the send is saved.
|
* Handles the event when the send is created.
|
||||||
*/
|
*/
|
||||||
onSendSaved() {
|
async onSendCreated(send: SendView) {
|
||||||
|
await this.router.navigate(["/send-created"], {
|
||||||
|
queryParams: { sendId: send.id },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the event when the send is updated.
|
||||||
|
*/
|
||||||
|
onSendUpdated(send: SendView) {
|
||||||
this.location.back();
|
this.location.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
<main class="tw-top-0">
|
<main class="tw-top-0">
|
||||||
<popup-page>
|
<popup-page>
|
||||||
<popup-header slot="header" [pageTitle]="'createdSend' | i18n" showBackButton>
|
<popup-header
|
||||||
|
slot="header"
|
||||||
|
[pageTitle]="'createdSend' | i18n"
|
||||||
|
showBackButton
|
||||||
|
[backAction]="close.bind(this)"
|
||||||
|
>
|
||||||
<ng-container slot="end">
|
<ng-container slot="end">
|
||||||
<app-pop-out></app-pop-out>
|
<app-pop-out></app-pop-out>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CommonModule, Location } from "@angular/common";
|
import { CommonModule, Location } from "@angular/common";
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { ActivatedRoute, RouterLink } from "@angular/router";
|
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
|
||||||
import { RouterTestingModule } from "@angular/router/testing";
|
import { RouterTestingModule } from "@angular/router/testing";
|
||||||
import { MockProxy, mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
import { of } from "rxjs";
|
import { of } from "rxjs";
|
||||||
@ -33,6 +33,7 @@ describe("SendCreatedComponent", () => {
|
|||||||
let location: MockProxy<Location>;
|
let location: MockProxy<Location>;
|
||||||
let activatedRoute: MockProxy<ActivatedRoute>;
|
let activatedRoute: MockProxy<ActivatedRoute>;
|
||||||
let environmentService: MockProxy<EnvironmentService>;
|
let environmentService: MockProxy<EnvironmentService>;
|
||||||
|
let router: MockProxy<Router>;
|
||||||
|
|
||||||
const sendId = "test-send-id";
|
const sendId = "test-send-id";
|
||||||
const deletionDate = new Date();
|
const deletionDate = new Date();
|
||||||
@ -52,6 +53,7 @@ describe("SendCreatedComponent", () => {
|
|||||||
location = mock<Location>();
|
location = mock<Location>();
|
||||||
activatedRoute = mock<ActivatedRoute>();
|
activatedRoute = mock<ActivatedRoute>();
|
||||||
environmentService = mock<EnvironmentService>();
|
environmentService = mock<EnvironmentService>();
|
||||||
|
router = mock<Router>();
|
||||||
Object.defineProperty(environmentService, "environment$", {
|
Object.defineProperty(environmentService, "environment$", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })),
|
get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })),
|
||||||
@ -89,6 +91,7 @@ describe("SendCreatedComponent", () => {
|
|||||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||||
{ provide: EnvironmentService, useValue: environmentService },
|
{ provide: EnvironmentService, useValue: environmentService },
|
||||||
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
|
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
@ -109,10 +112,10 @@ describe("SendCreatedComponent", () => {
|
|||||||
expect(component["daysAvailable"]).toBe(7);
|
expect(component["daysAvailable"]).toBe(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should navigate back on close", () => {
|
it("should navigate back to send list on close", async () => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
component.close();
|
await component.close();
|
||||||
expect(location.back).toHaveBeenCalled();
|
expect(router.navigate).toHaveBeenCalledWith(["/tabs/send"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getDaysAvailable", () => {
|
describe("getDaysAvailable", () => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CommonModule, Location } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { ActivatedRoute, RouterLink } from "@angular/router";
|
import { ActivatedRoute, Router, RouterLink, RouterModule } from "@angular/router";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
@ -30,6 +30,7 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
|
|||||||
PopupHeaderComponent,
|
PopupHeaderComponent,
|
||||||
PopupPageComponent,
|
PopupPageComponent,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
|
RouterModule,
|
||||||
PopupFooterComponent,
|
PopupFooterComponent,
|
||||||
IconModule,
|
IconModule,
|
||||||
],
|
],
|
||||||
@ -45,10 +46,11 @@ export class SendCreatedComponent {
|
|||||||
private sendService: SendService,
|
private sendService: SendService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private location: Location,
|
private router: Router,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
) {
|
) {
|
||||||
const sendId = this.route.snapshot.queryParamMap.get("sendId");
|
const sendId = this.route.snapshot.queryParamMap.get("sendId");
|
||||||
|
|
||||||
this.sendService.sendViews$.pipe(takeUntilDestroyed()).subscribe((sendViews) => {
|
this.sendService.sendViews$.pipe(takeUntilDestroyed()).subscribe((sendViews) => {
|
||||||
this.send = sendViews.find((s) => s.id === sendId);
|
this.send = sendViews.find((s) => s.id === sendId);
|
||||||
if (this.send) {
|
if (this.send) {
|
||||||
@ -62,8 +64,8 @@ export class SendCreatedComponent {
|
|||||||
return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60 * 24)));
|
return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60 * 24)));
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
async close() {
|
||||||
this.location.back();
|
await this.router.navigate(["/tabs/send"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyLink() {
|
async copyLink() {
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<bit-callout *ngIf="sendsDisabled" [title]="'sendDisabled' | i18n">
|
<bit-callout *ngIf="sendsDisabled" [title]="'sendDisabled' | i18n">
|
||||||
{{ "sendDisabledWarning" | i18n }}
|
{{ "sendDisabledWarning" | i18n }}
|
||||||
</bit-callout>
|
</bit-callout>
|
||||||
<ng-container *ngIf="!sendsDisabled">
|
<ng-container *ngIf="listState !== sendState.Empty">
|
||||||
<tools-send-search></tools-send-search>
|
<tools-send-search></tools-send-search>
|
||||||
<app-send-list-filters></app-send-list-filters>
|
<app-send-list-filters></app-send-list-filters>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -23,7 +23,11 @@
|
|||||||
<bit-no-items [icon]="noItemIcon" class="tw-text-main">
|
<bit-no-items [icon]="noItemIcon" class="tw-text-main">
|
||||||
<ng-container slot="title">{{ "sendsNoItemsTitle" | i18n }}</ng-container>
|
<ng-container slot="title">{{ "sendsNoItemsTitle" | i18n }}</ng-container>
|
||||||
<ng-container slot="description">{{ "sendsNoItemsMessage" | i18n }}</ng-container>
|
<ng-container slot="description">{{ "sendsNoItemsMessage" | i18n }}</ng-container>
|
||||||
<tools-new-send-dropdown *ngIf="!sendsDisabled" slot="button"></tools-new-send-dropdown>
|
<tools-new-send-dropdown
|
||||||
|
[hideIcon]="true"
|
||||||
|
*ngIf="!sendsDisabled"
|
||||||
|
slot="button"
|
||||||
|
></tools-new-send-dropdown>
|
||||||
</bit-no-items>
|
</bit-no-items>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -17,10 +17,14 @@
|
|||||||
{{ description }}
|
{{ description }}
|
||||||
</div>
|
</div>
|
||||||
<bit-item-group>
|
<bit-item-group>
|
||||||
<cdk-virtual-scroll-viewport [itemSize]="ItemHeight">
|
<cdk-virtual-scroll-viewport
|
||||||
|
[itemSize]="ItemHeight"
|
||||||
|
class="tw-overflow-visible [&>.cdk-virtual-scroll-content-wrapper]:[contain:layout_style]"
|
||||||
|
>
|
||||||
<bit-item *cdkVirtualFor="let cipher of ciphers">
|
<bit-item *cdkVirtualFor="let cipher of ciphers">
|
||||||
<a
|
<button
|
||||||
bit-item-content
|
bit-item-content
|
||||||
|
type="button"
|
||||||
(click)="onViewCipher(cipher)"
|
(click)="onViewCipher(cipher)"
|
||||||
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
||||||
class="{{ ItemHeightClass }}"
|
class="{{ ItemHeightClass }}"
|
||||||
@ -40,7 +44,7 @@
|
|||||||
[appA11yTitle]="'attachments' | i18n"
|
[appA11yTitle]="'attachments' | i18n"
|
||||||
></i>
|
></i>
|
||||||
<span slot="secondary">{{ cipher.subTitle }}</span>
|
<span slot="secondary">{{ cipher.subTitle }}</span>
|
||||||
</a>
|
</button>
|
||||||
<ng-container slot="end">
|
<ng-container slot="end">
|
||||||
<bit-item-action *ngIf="showAutofillButton">
|
<bit-item-action *ngIf="showAutofillButton">
|
||||||
<button
|
<button
|
||||||
|
@ -66,7 +66,7 @@
|
|||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"https-proxy-agent": "7.0.5",
|
"https-proxy-agent": "7.0.5",
|
||||||
"inquirer": "8.2.6",
|
"inquirer": "8.2.6",
|
||||||
"jsdom": "24.1.3",
|
"jsdom": "25.0.1",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"koa": "2.15.0",
|
"koa": "2.15.0",
|
||||||
"koa-bodyparser": "4.4.1",
|
"koa-bodyparser": "4.4.1",
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
} from "@bitwarden/admin-console/common";
|
} from "@bitwarden/admin-console/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
import { Response } from "../../models/response";
|
import { Response } from "../../models/response";
|
||||||
@ -12,6 +13,7 @@ export class ConfirmCommand {
|
|||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
|
private encryptService: EncryptService,
|
||||||
private organizationUserApiService: OrganizationUserApiService,
|
private organizationUserApiService: OrganizationUserApiService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -53,7 +55,7 @@ export class ConfirmCommand {
|
|||||||
}
|
}
|
||||||
const publicKeyResponse = await this.apiService.getUserPublicKey(orgUser.userId);
|
const publicKeyResponse = await this.apiService.getUserPublicKey(orgUser.userId);
|
||||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||||
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey);
|
const key = await this.encryptService.rsaEncrypt(orgKey.key, publicKey);
|
||||||
const req = new OrganizationUserConfirmRequest();
|
const req = new OrganizationUserConfirmRequest();
|
||||||
req.key = key.encryptedString;
|
req.key = key.encryptedString;
|
||||||
await this.organizationUserApiService.postOrganizationUserConfirm(
|
await this.organizationUserApiService.postOrganizationUserConfirm(
|
||||||
|
@ -117,6 +117,7 @@ export class OssServeConfigurator {
|
|||||||
this.confirmCommand = new ConfirmCommand(
|
this.confirmCommand = new ConfirmCommand(
|
||||||
this.serviceContainer.apiService,
|
this.serviceContainer.apiService,
|
||||||
this.serviceContainer.cryptoService,
|
this.serviceContainer.cryptoService,
|
||||||
|
this.serviceContainer.encryptService,
|
||||||
this.serviceContainer.organizationUserApiService,
|
this.serviceContainer.organizationUserApiService,
|
||||||
);
|
);
|
||||||
this.restoreCommand = new RestoreCommand(this.serviceContainer.cipherService);
|
this.restoreCommand = new RestoreCommand(this.serviceContainer.cipherService);
|
||||||
|
@ -498,8 +498,6 @@ export class ServiceContainer {
|
|||||||
|
|
||||||
this.providerService = new ProviderService(this.stateProvider);
|
this.providerService = new ProviderService(this.stateProvider);
|
||||||
|
|
||||||
this.organizationUserApiService = new DefaultOrganizationUserApiService(this.apiService);
|
|
||||||
|
|
||||||
this.policyApiService = new PolicyApiService(this.policyService, this.apiService);
|
this.policyApiService = new PolicyApiService(this.policyService, this.apiService);
|
||||||
|
|
||||||
this.keyConnectorService = new KeyConnectorService(
|
this.keyConnectorService = new KeyConnectorService(
|
||||||
@ -536,6 +534,7 @@ export class ServiceContainer {
|
|||||||
this.accountService,
|
this.accountService,
|
||||||
this.masterPasswordService,
|
this.masterPasswordService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
|
this.encryptService,
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
@ -778,6 +777,11 @@ export class ServiceContainer {
|
|||||||
this.organizationApiService = new OrganizationApiService(this.apiService, this.syncService);
|
this.organizationApiService = new OrganizationApiService(this.apiService, this.syncService);
|
||||||
|
|
||||||
this.providerApiService = new ProviderApiService(this.apiService);
|
this.providerApiService = new ProviderApiService(this.apiService);
|
||||||
|
|
||||||
|
this.organizationUserApiService = new DefaultOrganizationUserApiService(
|
||||||
|
this.apiService,
|
||||||
|
this.configService,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
|
@ -415,6 +415,7 @@ export class VaultProgram extends BaseProgram {
|
|||||||
const command = new ConfirmCommand(
|
const command = new ConfirmCommand(
|
||||||
this.serviceContainer.apiService,
|
this.serviceContainer.apiService,
|
||||||
this.serviceContainer.cryptoService,
|
this.serviceContainer.cryptoService,
|
||||||
|
this.serviceContainer.encryptService,
|
||||||
this.serviceContainer.organizationUserApiService,
|
this.serviceContainer.organizationUserApiService,
|
||||||
);
|
);
|
||||||
const response = await command.run(object, id, cmd);
|
const response = await command.run(object, id, cmd);
|
||||||
|
1
apps/desktop/desktop_native/.gitignore
vendored
1
apps/desktop/desktop_native/.gitignore
vendored
@ -4,3 +4,4 @@ index.node
|
|||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
*.node
|
*.node
|
||||||
|
dist
|
||||||
|
309
apps/desktop/desktop_native/Cargo.lock
generated
309
apps/desktop/desktop_native/Cargo.lock
generated
@ -210,9 +210,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.3.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
@ -304,9 +304,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.1.21"
|
version = "1.1.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0"
|
checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
@ -481,6 +481,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.3.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive-new"
|
name = "derive-new"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@ -502,10 +511,14 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"cbc",
|
"cbc",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
|
"dirs",
|
||||||
|
"futures",
|
||||||
"gio",
|
"gio",
|
||||||
|
"interprocess",
|
||||||
"keytar",
|
"keytar",
|
||||||
"libc",
|
"libc",
|
||||||
"libsecret",
|
"libsecret",
|
||||||
|
"log",
|
||||||
"rand",
|
"rand",
|
||||||
"retry",
|
"retry",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
@ -514,6 +527,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"typenum",
|
"typenum",
|
||||||
"widestring",
|
"widestring",
|
||||||
"windows",
|
"windows",
|
||||||
@ -530,6 +544,22 @@ dependencies = [
|
|||||||
"napi",
|
"napi",
|
||||||
"napi-build",
|
"napi-build",
|
||||||
"napi-derive",
|
"napi-derive",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "desktop_proxy"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"desktop_core",
|
||||||
|
"embed_plist",
|
||||||
|
"futures",
|
||||||
|
"log",
|
||||||
|
"simplelog",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -542,6 +572,27 @@ dependencies = [
|
|||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "5.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dlib"
|
name = "dlib"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@ -551,12 +602,24 @@ dependencies = [
|
|||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "doctest-file"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "downcast-rs"
|
name = "downcast-rs"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "embed_plist"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "endi"
|
name = "endi"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@ -645,6 +708,21 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
@ -652,6 +730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -719,6 +798,7 @@ version = "0.3.30"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
@ -913,6 +993,27 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "interprocess"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2f4e4a06d42fab3e85ab1b419ad32b09eab58b901d40c57935ff92db3287a13"
|
||||||
|
dependencies = [
|
||||||
|
"doctest-file",
|
||||||
|
"futures-core",
|
||||||
|
"libc",
|
||||||
|
"recvmsg",
|
||||||
|
"tokio",
|
||||||
|
"widestring",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keytar"
|
name = "keytar"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@ -950,6 +1051,16 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsecret"
|
name = "libsecret"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -1038,10 +1149,21 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi"
|
name = "mio"
|
||||||
version = "2.16.6"
|
version = "0.8.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfc300228808a0e6aea5a58115c82889240bcf8dab16fc25ad675b33e454b368"
|
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "napi"
|
||||||
|
version = "2.16.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "633e41b2b983cf7983134f0c50986ca524d0caf38a2c6fc893ea3fa2e26abb0c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"ctor",
|
"ctor",
|
||||||
@ -1059,9 +1181,9 @@ checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi-derive"
|
name = "napi-derive"
|
||||||
version = "2.16.5"
|
version = "2.16.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e0e034ddf6155192cf83f267ede763fe6c164dfa9971585436b16173718d94c4"
|
checksum = "70a8a778fd367b13c64232e58632514b795514ece491ce136d96e976d34a3eb8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"convert_case",
|
"convert_case",
|
||||||
@ -1130,6 +1252,12 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
@ -1140,6 +1268,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_threads"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc-sys"
|
name = "objc-sys"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
@ -1250,9 +1387,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.19.0"
|
version = "1.20.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-stream"
|
name = "ordered-stream"
|
||||||
@ -1357,6 +1503,18 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.20"
|
version = "0.2.20"
|
||||||
@ -1433,19 +1591,36 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "recvmsg"
|
||||||
version = "0.5.5"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62871f2d65009c0256aed1b9cfeeb8ac272833c404e13d53d400cd0dad7a2ac0"
|
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "redox_users"
|
||||||
version = "1.10.6"
|
version = "0.4.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"libredox",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@ -1455,9 +1630,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.7"
|
version = "0.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@ -1466,9 +1641,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.4"
|
version = "0.8.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "retry"
|
name = "retry"
|
||||||
@ -1578,9 +1753,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "0.6.7"
|
version = "0.6.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
|
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@ -1622,6 +1797,17 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simplelog"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"termcolor",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
@ -1637,6 +1823,16 @@ version = "1.13.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "static_assertions"
|
name = "static_assertions"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@ -1645,9 +1841,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.77"
|
version = "2.0.79"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1715,6 +1911,39 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"itoa",
|
||||||
|
"libc",
|
||||||
|
"num-conv",
|
||||||
|
"num_threads",
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.38.0"
|
version = "1.38.0"
|
||||||
@ -1723,9 +1952,13 @@ checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1739,6 +1972,19 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-util"
|
||||||
|
version = "0.7.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.19"
|
version = "0.8.19"
|
||||||
@ -2034,6 +2280,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.48.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.48.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
@ -2175,9 +2430,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.6.19"
|
version = "0.6.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587"
|
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["napi", "core"]
|
members = ["napi", "core", "proxy"]
|
||||||
|
68
apps/desktop/desktop_native/build.js
Normal file
68
apps/desktop/desktop_native/build.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const child_process = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const process = require("process");
|
||||||
|
|
||||||
|
let crossPlatform = process.argv.length > 2 && process.argv[2] === "cross-platform";
|
||||||
|
|
||||||
|
function buildNapiModule(target, release = true) {
|
||||||
|
const targetArg = target ? `--target ${target}` : "";
|
||||||
|
const releaseArg = release ? "--release" : "";
|
||||||
|
return child_process.execSync(`npm run build -- ${releaseArg} ${targetArg}`, { stdio: 'inherit', cwd: path.join(__dirname, "napi") });
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProxyBin(target, release = true) {
|
||||||
|
const targetArg = target ? `--target ${target}` : "";
|
||||||
|
const releaseArg = release ? "--release" : "";
|
||||||
|
return child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!crossPlatform) {
|
||||||
|
console.log("Building native modules in debug mode for the native architecture");
|
||||||
|
buildNapiModule(false, false);
|
||||||
|
buildProxyBin(false, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that targets contains pairs of [rust target, node arch]
|
||||||
|
// We do this to move the output binaries to a location that can
|
||||||
|
// be easily accessed from electron-builder using ${os} and ${arch}
|
||||||
|
let targets = [];
|
||||||
|
switch (process.platform) {
|
||||||
|
case "win32":
|
||||||
|
targets = [
|
||||||
|
["i686-pc-windows-msvc", 'ia32'],
|
||||||
|
["x86_64-pc-windows-msvc", 'x64'],
|
||||||
|
["aarch64-pc-windows-msvc", 'arm64']
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "darwin":
|
||||||
|
targets = [
|
||||||
|
["x86_64-apple-darwin", 'x64'],
|
||||||
|
["aarch64-apple-darwin", 'arm64']
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
targets = [
|
||||||
|
['x86_64-unknown-linux-musl', 'x64']
|
||||||
|
];
|
||||||
|
|
||||||
|
process.env["PKG_CONFIG_ALLOW_CROSS"] = "1";
|
||||||
|
process.env["PKG_CONFIG_ALL_STATIC"] = "1";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Cross building native modules for the targets: ", targets.map(([target, _]) => target).join(", "));
|
||||||
|
|
||||||
|
fs.mkdirSync(path.join(__dirname, "dist"), { recursive: true });
|
||||||
|
|
||||||
|
targets.forEach(([target, nodeArch]) => {
|
||||||
|
buildNapiModule(target);
|
||||||
|
buildProxyBin(target);
|
||||||
|
|
||||||
|
const ext = process.platform === "win32" ? ".exe" : "";
|
||||||
|
fs.copyFileSync(path.join(__dirname, "target", target, "release", `desktop_proxy${ext}`), path.join(__dirname, "dist", `desktop_proxy.${process.platform}-${nodeArch}${ext}`));
|
||||||
|
});
|
@ -6,9 +6,21 @@ version = "0.0.0"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["sys"]
|
||||||
manual_test = []
|
manual_test = []
|
||||||
|
|
||||||
|
sys = [
|
||||||
|
"dep:widestring",
|
||||||
|
"dep:windows",
|
||||||
|
"dep:core-foundation",
|
||||||
|
"dep:security-framework",
|
||||||
|
"dep:security-framework-sys",
|
||||||
|
"dep:gio",
|
||||||
|
"dep:libsecret",
|
||||||
|
"dep:zbus",
|
||||||
|
"dep:zbus_polkit",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes = "=0.8.4"
|
aes = "=0.8.4"
|
||||||
anyhow = "=1.0.86"
|
anyhow = "=1.0.86"
|
||||||
@ -17,17 +29,22 @@ arboard = { version = "=3.4.1", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
base64 = "=0.22.1"
|
base64 = "=0.22.1"
|
||||||
cbc = { version = "=0.1.2", features = ["alloc"] }
|
cbc = { version = "=0.1.2", features = ["alloc"] }
|
||||||
|
dirs = "=5.0.1"
|
||||||
|
futures = "=0.3.30"
|
||||||
|
interprocess = { version = "=2.2.1", features = ["tokio"] }
|
||||||
libc = "=0.2.155"
|
libc = "=0.2.155"
|
||||||
|
log = "=0.4.22"
|
||||||
rand = "=0.8.5"
|
rand = "=0.8.5"
|
||||||
retry = "=2.0.0"
|
retry = "=2.0.0"
|
||||||
scopeguard = "=1.2.0"
|
scopeguard = "=1.2.0"
|
||||||
sha2 = "=0.10.8"
|
sha2 = "=0.10.8"
|
||||||
thiserror = "=1.0.61"
|
thiserror = "=1.0.61"
|
||||||
tokio = { version = "=1.38.0", features = ["io-util", "sync", "macros"] }
|
tokio = { version = "=1.38.0", features = ["io-util", "sync", "macros"] }
|
||||||
|
tokio-util = "=0.7.11"
|
||||||
typenum = "=1.17.0"
|
typenum = "=1.17.0"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
widestring = "=1.1.0"
|
widestring = { version = "=1.1.0", optional = true }
|
||||||
windows = { version = "=0.57.0", features = [
|
windows = { version = "=0.57.0", features = [
|
||||||
"Foundation",
|
"Foundation",
|
||||||
"Security_Credentials_UI",
|
"Security_Credentials_UI",
|
||||||
@ -38,18 +55,18 @@ windows = { version = "=0.57.0", features = [
|
|||||||
"Win32_System_WinRT",
|
"Win32_System_WinRT",
|
||||||
"Win32_UI_Input_KeyboardAndMouse",
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
] }
|
], optional = true }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dev-dependencies]
|
[target.'cfg(windows)'.dev-dependencies]
|
||||||
keytar = "=0.1.6"
|
keytar = "=0.1.6"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
core-foundation = "=0.9.4"
|
core-foundation = { version = "=0.9.4", optional = true }
|
||||||
security-framework = "=2.11.0"
|
security-framework = { version = "=2.11.0", optional = true }
|
||||||
security-framework-sys = "=2.11.0"
|
security-framework-sys = { version = "=2.11.0", optional = true }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
gio = "=0.19.5"
|
gio = { version = "=0.19.5", optional = true }
|
||||||
libsecret = "=0.5.0"
|
libsecret = { version = "=0.5.0", optional = true }
|
||||||
zbus = "=4.3.1"
|
zbus = { version = "=4.3.1", optional = true }
|
||||||
zbus_polkit = "=4.0.0"
|
zbus_polkit = { version = "=4.0.0", optional = true }
|
||||||
|
70
apps/desktop/desktop_native/core/src/ipc/client.rs
Normal file
70
apps/desktop/desktop_native/core/src/ipc/client.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use interprocess::local_socket::{
|
||||||
|
tokio::{prelude::*, Stream},
|
||||||
|
GenericFilePath, ToFsName,
|
||||||
|
};
|
||||||
|
use log::{error, info};
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
|
use crate::ipc::NATIVE_MESSAGING_BUFFER_SIZE;
|
||||||
|
|
||||||
|
pub async fn connect(
|
||||||
|
path: PathBuf,
|
||||||
|
send: tokio::sync::mpsc::Sender<String>,
|
||||||
|
mut recv: tokio::sync::mpsc::Receiver<String>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
info!("Attempting to connect to {}", path.display());
|
||||||
|
|
||||||
|
let name = path.as_os_str().to_fs_name::<GenericFilePath>()?;
|
||||||
|
let mut conn = Stream::connect(name).await?;
|
||||||
|
|
||||||
|
info!("Connected to {}", path.display());
|
||||||
|
|
||||||
|
// This `connected` and the latter `disconnected` messages are the only ones that
|
||||||
|
// are sent from the Rust IPC code and not just forwarded from the desktop app.
|
||||||
|
// As it's only two, we hardcode the JSON values to avoid pulling in a JSON library.
|
||||||
|
send.send("{\"command\":\"connected\"}".to_owned()).await?;
|
||||||
|
|
||||||
|
let mut buffer = vec![0; NATIVE_MESSAGING_BUFFER_SIZE];
|
||||||
|
|
||||||
|
// Listen to IPC messages
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
// Forward messages to the IPC server
|
||||||
|
msg = recv.recv() => {
|
||||||
|
match msg {
|
||||||
|
Some(msg) => {
|
||||||
|
conn.write_all(msg.as_bytes()).await?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
info!("Client channel closed");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Forward messages from the IPC server
|
||||||
|
res = conn.read(&mut buffer[..]) => {
|
||||||
|
match res {
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error reading from IPC server: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(0) => {
|
||||||
|
info!("Connection closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(n) => {
|
||||||
|
let message = String::from_utf8_lossy(&buffer[..n]).to_string();
|
||||||
|
send.send(message).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = send.send("{\"command\":\"disconnected\"}".to_owned()).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
66
apps/desktop/desktop_native/core/src/ipc/mod.rs
Normal file
66
apps/desktop/desktop_native/core/src/ipc/mod.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
pub mod client;
|
||||||
|
pub mod server;
|
||||||
|
|
||||||
|
/// The maximum size of a message that can be sent over IPC.
|
||||||
|
/// According to the documentation, the maximum size sent to the browser is 1MB.
|
||||||
|
/// While the maximum size sent from the browser to the native messaging host is 4GB.
|
||||||
|
///
|
||||||
|
/// Currently we are setting the maximum both ways to be 1MB.
|
||||||
|
///
|
||||||
|
/// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#app_side
|
||||||
|
/// https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging#native-messaging-host-protocol
|
||||||
|
pub const NATIVE_MESSAGING_BUFFER_SIZE: usize = 1024 * 1024;
|
||||||
|
|
||||||
|
/// The maximum number of messages that can be buffered in a channel.
|
||||||
|
/// This number is more or less arbitrary and can be adjusted as needed,
|
||||||
|
/// but ideally the messages should be processed as quickly as possible.
|
||||||
|
pub const MESSAGE_CHANNEL_BUFFER: usize = 32;
|
||||||
|
|
||||||
|
/// Resolve the path to the IPC socket.
|
||||||
|
pub fn path(name: &str) -> std::path::PathBuf {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// Use a unique IPC pipe //./pipe/xxxxxxxxxxxxxxxxx.app.bitwarden per user.
|
||||||
|
// Hashing prevents problems with reserved characters and file length limitations.
|
||||||
|
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||||
|
use sha2::Digest;
|
||||||
|
let home = dirs::home_dir().unwrap();
|
||||||
|
let hash = sha2::Sha256::digest(home.as_os_str().as_encoded_bytes());
|
||||||
|
let hash_b64 = URL_SAFE_NO_PAD.encode(hash.as_slice());
|
||||||
|
|
||||||
|
format!(r"\\.\pipe\{hash_b64}.app.{name}").into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let mut home = dirs::home_dir().unwrap();
|
||||||
|
|
||||||
|
// When running in an unsandboxed environment, path is: /Users/<user>/
|
||||||
|
// While running sandboxed, it's different: /Users/<user>/Library/Containers/com.bitwarden.desktop/Data
|
||||||
|
//
|
||||||
|
// We want to use App Groups in /Users/<user>/Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop,
|
||||||
|
// so we need to remove all the components after the user.
|
||||||
|
// Note that we subtract 3 because the root directory is counted as a component (/, Users, <user>).
|
||||||
|
let num_components = home.components().count();
|
||||||
|
for _ in 0..num_components - 3 {
|
||||||
|
home.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
let tmp = home.join("Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop/tmp");
|
||||||
|
|
||||||
|
// The tmp directory might not exist, so create it
|
||||||
|
let _ = std::fs::create_dir_all(&tmp);
|
||||||
|
tmp.join(format!("app.{name}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// On Linux, we use the user's cache directory.
|
||||||
|
let home = dirs::cache_dir().unwrap();
|
||||||
|
let path_dir = home.join("com.bitwarden.desktop");
|
||||||
|
|
||||||
|
// The chache directory might not exist, so create it
|
||||||
|
let _ = std::fs::create_dir_all(&path_dir);
|
||||||
|
path_dir.join(format!("app.{name}"))
|
||||||
|
}
|
||||||
|
}
|
232
apps/desktop/desktop_native/core/src/ipc/server.rs
Normal file
232
apps/desktop/desktop_native/core/src/ipc/server.rs
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
use std::{error::Error, path::Path, vec};
|
||||||
|
|
||||||
|
use futures::TryFutureExt;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use interprocess::local_socket::{tokio::prelude::*, GenericFilePath, ListenerOptions};
|
||||||
|
use log::{error, info};
|
||||||
|
use tokio::{
|
||||||
|
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
|
||||||
|
sync::{broadcast, mpsc},
|
||||||
|
};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
use super::{MESSAGE_CHANNEL_BUFFER, NATIVE_MESSAGING_BUFFER_SIZE};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Message {
|
||||||
|
pub client_id: u32,
|
||||||
|
pub kind: MessageType,
|
||||||
|
// This value should be Some for MessageType::Message and None for the rest
|
||||||
|
pub message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MessageType {
|
||||||
|
Connected,
|
||||||
|
Disconnected,
|
||||||
|
Message,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Server {
|
||||||
|
cancel_token: CancellationToken,
|
||||||
|
server_to_clients_send: broadcast::Sender<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
/// Create and start the IPC server without blocking.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// - `name`: The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
|
||||||
|
/// - `client_to_server_send`: This [`mpsc::Sender<Message>`] will receive all the [`Message`]'s that the clients send to this server.
|
||||||
|
pub fn start(
|
||||||
|
path: &Path,
|
||||||
|
client_to_server_send: mpsc::Sender<Message>,
|
||||||
|
) -> Result<Self, Box<dyn Error>> {
|
||||||
|
// If the unix socket file already exists, we get an error when trying to bind to it. So we remove it first.
|
||||||
|
// Any processes that were using the old socket should remain connected to it but any new connections will use the new socket.
|
||||||
|
if !cfg!(windows) {
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = path.as_os_str().to_fs_name::<GenericFilePath>()?;
|
||||||
|
let opts = ListenerOptions::new().name(name);
|
||||||
|
let listener = opts.create_tokio()?;
|
||||||
|
|
||||||
|
// This broadcast channel is used for sending messages to all connected clients, and so the sender
|
||||||
|
// will be stored in the server while the receiver will be cloned and passed to each client handler.
|
||||||
|
let (server_to_clients_send, server_to_clients_recv) =
|
||||||
|
broadcast::channel::<String>(MESSAGE_CHANNEL_BUFFER);
|
||||||
|
|
||||||
|
// This cancellation token allows us to cleanly stop the server and all the spawned
|
||||||
|
// tasks without having to wait on all the pending tasks finalizing first
|
||||||
|
let cancel_token = CancellationToken::new();
|
||||||
|
|
||||||
|
// Create the server and start listening for incoming connections
|
||||||
|
// in a separate task to avoid blocking the current task
|
||||||
|
let server = Server {
|
||||||
|
cancel_token: cancel_token.clone(),
|
||||||
|
server_to_clients_send,
|
||||||
|
};
|
||||||
|
tokio::spawn(listen_incoming(
|
||||||
|
listener,
|
||||||
|
client_to_server_send,
|
||||||
|
server_to_clients_recv,
|
||||||
|
cancel_token,
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a message over the IPC server to all the connected clients
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The number of clients that the message was sent to. Note that the number of messages
|
||||||
|
/// sent may be less than the number of connected clients if some clients disconnect while
|
||||||
|
/// the message is being sent.
|
||||||
|
pub fn send(&self, message: String) -> Result<usize> {
|
||||||
|
let sent = self.server_to_clients_send.send(message)?;
|
||||||
|
Ok(sent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the IPC server.
|
||||||
|
pub fn stop(&self) {
|
||||||
|
self.cancel_token.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Server {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn listen_incoming(
|
||||||
|
listener: LocalSocketListener,
|
||||||
|
client_to_server_send: mpsc::Sender<Message>,
|
||||||
|
server_to_clients_recv: broadcast::Receiver<String>,
|
||||||
|
cancel_token: CancellationToken,
|
||||||
|
) {
|
||||||
|
// We use a simple incrementing ID for each client
|
||||||
|
let mut next_client_id = 1_u32;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel_token.cancelled() => {
|
||||||
|
info!("IPC server cancelled.");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
|
||||||
|
// A new client connection has been established
|
||||||
|
msg = listener.accept() => {
|
||||||
|
match msg {
|
||||||
|
Ok(client_stream) => {
|
||||||
|
let client_id = next_client_id;
|
||||||
|
next_client_id += 1;
|
||||||
|
|
||||||
|
let future = handle_connection(
|
||||||
|
client_stream,
|
||||||
|
client_to_server_send.clone(),
|
||||||
|
// We resubscribe to the receiver here so this task can have it's own copy
|
||||||
|
// Note that this copy will only receive messages sent after this point,
|
||||||
|
// but that is okay, realistically we don't want any messages before we get a chance
|
||||||
|
// to send the connected message to the client, which is done inside [`handle_connection`]
|
||||||
|
server_to_clients_recv.resubscribe(),
|
||||||
|
cancel_token.clone(),
|
||||||
|
client_id
|
||||||
|
);
|
||||||
|
tokio::spawn(future.map_err(|e| {
|
||||||
|
error!("Error handling connection: {}", e)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error accepting connection: {}", e);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_connection(
|
||||||
|
mut client_stream: impl AsyncRead + AsyncWrite + Unpin,
|
||||||
|
client_to_server_send: mpsc::Sender<Message>,
|
||||||
|
mut server_to_clients_recv: broadcast::Receiver<String>,
|
||||||
|
cancel_token: CancellationToken,
|
||||||
|
client_id: u32,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
client_to_server_send
|
||||||
|
.send(Message {
|
||||||
|
client_id,
|
||||||
|
kind: MessageType::Connected,
|
||||||
|
message: None,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut buf = vec![0u8; NATIVE_MESSAGING_BUFFER_SIZE];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel_token.cancelled() => {
|
||||||
|
info!("Client {client_id} cancelled.");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Forward messages to the IPC clients
|
||||||
|
msg = server_to_clients_recv.recv() => {
|
||||||
|
match msg {
|
||||||
|
Ok(msg) => {
|
||||||
|
client_stream.write_all(msg.as_bytes()).await?;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
info!("Error reading message: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Forwards messages from the IPC clients to the server
|
||||||
|
// Note that we also send connect and disconnect events so that
|
||||||
|
// the server can keep track of multiple clients
|
||||||
|
result = client_stream.read(&mut buf) => {
|
||||||
|
match result {
|
||||||
|
Err(e) => {
|
||||||
|
info!("Error reading from client {client_id}: {e}");
|
||||||
|
|
||||||
|
client_to_server_send.send(Message {
|
||||||
|
client_id,
|
||||||
|
kind: MessageType::Disconnected,
|
||||||
|
message: None,
|
||||||
|
}).await?;
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Ok(0) => {
|
||||||
|
info!("Client {client_id} disconnected.");
|
||||||
|
|
||||||
|
client_to_server_send.send(Message {
|
||||||
|
client_id,
|
||||||
|
kind: MessageType::Disconnected,
|
||||||
|
message: None,
|
||||||
|
}).await?;
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Ok(size) => {
|
||||||
|
let msg = std::str::from_utf8(&buf[..size])?;
|
||||||
|
|
||||||
|
client_to_server_send.send(Message {
|
||||||
|
client_id,
|
||||||
|
kind: MessageType::Message,
|
||||||
|
message: Some(msg.to_string()),
|
||||||
|
}).await?;
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,7 +1,13 @@
|
|||||||
|
#[cfg(feature = "sys")]
|
||||||
pub mod biometric;
|
pub mod biometric;
|
||||||
|
#[cfg(feature = "sys")]
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod ipc;
|
||||||
|
#[cfg(feature = "sys")]
|
||||||
pub mod password;
|
pub mod password;
|
||||||
|
#[cfg(feature = "sys")]
|
||||||
pub mod process_isolation;
|
pub mod process_isolation;
|
||||||
|
#[cfg(feature = "sys")]
|
||||||
pub mod powermonitor;
|
pub mod powermonitor;
|
||||||
|
@ -16,8 +16,10 @@ manual_test = []
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "=1.0.86"
|
anyhow = "=1.0.86"
|
||||||
desktop_core = { path = "../core" }
|
desktop_core = { path = "../core" }
|
||||||
napi = { version = "=2.16.6", features = ["async"] }
|
napi = { version = "=2.16.7", features = ["async"] }
|
||||||
napi-derive = "=2.16.5"
|
napi-derive = "=2.16.6"
|
||||||
|
tokio = { version = "1.38.0" }
|
||||||
|
tokio-util = "0.7.11"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
napi-build = "=2.1.3"
|
napi-build = "=2.1.3"
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
||||||
const child_process = require("child_process");
|
|
||||||
const process = require("process");
|
|
||||||
|
|
||||||
let targets = [];
|
|
||||||
switch (process.platform) {
|
|
||||||
case "win32":
|
|
||||||
targets = ["i686-pc-windows-msvc", "x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "darwin":
|
|
||||||
targets = ["x86_64-apple-darwin", "aarch64-apple-darwin"];
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
targets = ['x86_64-unknown-linux-musl'];
|
|
||||||
process.env["PKG_CONFIG_ALLOW_CROSS"] = "1";
|
|
||||||
process.env["PKG_CONFIG_ALL_STATIC"] = "1";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
targets.forEach(target => {
|
|
||||||
child_process.execSync(`npm run build -- --target ${target}`, {stdio: 'inherit'});
|
|
||||||
});
|
|
30
apps/desktop/desktop_native/napi/index.d.ts
vendored
30
apps/desktop/desktop_native/napi/index.d.ts
vendored
@ -51,3 +51,33 @@ export namespace powermonitors {
|
|||||||
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
||||||
export function isLockMonitorAvailable(): Promise<boolean>
|
export function isLockMonitorAvailable(): Promise<boolean>
|
||||||
}
|
}
|
||||||
|
export namespace ipc {
|
||||||
|
export interface IpcMessage {
|
||||||
|
clientId: number
|
||||||
|
kind: IpcMessageType
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
export const enum IpcMessageType {
|
||||||
|
Connected = 0,
|
||||||
|
Disconnected = 1,
|
||||||
|
Message = 2
|
||||||
|
}
|
||||||
|
export class IpcServer {
|
||||||
|
/**
|
||||||
|
* Create and start the IPC server without blocking.
|
||||||
|
*
|
||||||
|
* @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
|
||||||
|
* @param callback This function will be called whenever a message is received from a client.
|
||||||
|
*/
|
||||||
|
static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise<IpcServer>
|
||||||
|
/** Stop the IPC server. */
|
||||||
|
stop(): void
|
||||||
|
/**
|
||||||
|
* Send a message over the IPC server to all the connected clients
|
||||||
|
*
|
||||||
|
* @return The number of clients that the message was sent to. Note that the number of messages
|
||||||
|
* actually received may be less, as some clients could disconnect before receiving the message.
|
||||||
|
*/
|
||||||
|
send(message: string): number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -206,10 +206,4 @@ if (!nativeBinding) {
|
|||||||
throw new Error(`Failed to load native binding`)
|
throw new Error(`Failed to load native binding`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { passwords, biometrics, clipboards, processisolations, powermonitors } = nativeBinding
|
module.exports = nativeBinding
|
||||||
|
|
||||||
module.exports.passwords = passwords
|
|
||||||
module.exports.biometrics = biometrics
|
|
||||||
module.exports.clipboards = clipboards
|
|
||||||
module.exports.processisolations = processisolations
|
|
||||||
module.exports.powermonitors = powermonitors
|
|
||||||
|
@ -3,9 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "napi build --release --platform --js false",
|
"build": "napi build --platform --js false",
|
||||||
"build:debug": "napi build --platform --js false",
|
|
||||||
"build:cross-platform": "node build.js",
|
|
||||||
"test": "cargo test"
|
"test": "cargo test"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -189,3 +189,103 @@ pub mod powermonitors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub mod ipc {
|
||||||
|
use desktop_core::ipc::server::{Message, MessageType};
|
||||||
|
use napi::threadsafe_function::{
|
||||||
|
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct IpcMessage {
|
||||||
|
pub client_id: u32,
|
||||||
|
pub kind: IpcMessageType,
|
||||||
|
pub message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Message> for IpcMessage {
|
||||||
|
fn from(message: Message) -> Self {
|
||||||
|
IpcMessage {
|
||||||
|
client_id: message.client_id,
|
||||||
|
kind: message.kind.into(),
|
||||||
|
message: message.message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub enum IpcMessageType {
|
||||||
|
Connected,
|
||||||
|
Disconnected,
|
||||||
|
Message,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MessageType> for IpcMessageType {
|
||||||
|
fn from(message_type: MessageType) -> Self {
|
||||||
|
match message_type {
|
||||||
|
MessageType::Connected => IpcMessageType::Connected,
|
||||||
|
MessageType::Disconnected => IpcMessageType::Disconnected,
|
||||||
|
MessageType::Message => IpcMessageType::Message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub struct IpcServer {
|
||||||
|
server: desktop_core::ipc::server::Server,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl IpcServer {
|
||||||
|
/// Create and start the IPC server without blocking.
|
||||||
|
///
|
||||||
|
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
|
||||||
|
/// @param callback This function will be called whenever a message is received from a client.
|
||||||
|
#[napi(factory)]
|
||||||
|
pub async fn listen(
|
||||||
|
name: String,
|
||||||
|
#[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")]
|
||||||
|
callback: ThreadsafeFunction<IpcMessage, ErrorStrategy::CalleeHandled>,
|
||||||
|
) -> napi::Result<Self> {
|
||||||
|
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(message) = recv.recv().await {
|
||||||
|
callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let path = desktop_core::ipc::path(&name);
|
||||||
|
|
||||||
|
let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| {
|
||||||
|
napi::Error::from_reason(format!(
|
||||||
|
"Error listening to server - Path: {path:?} - Error: {e} - {e:?}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(IpcServer { server })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the IPC server.
|
||||||
|
#[napi]
|
||||||
|
pub fn stop(&self) -> napi::Result<()> {
|
||||||
|
self.server.stop();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a message over the IPC server to all the connected clients
|
||||||
|
///
|
||||||
|
/// @return The number of clients that the message was sent to. Note that the number of messages
|
||||||
|
/// actually received may be less, as some clients could disconnect before receiving the message.
|
||||||
|
#[napi]
|
||||||
|
pub fn send(&self, message: String) -> napi::Result<u32> {
|
||||||
|
self.server
|
||||||
|
.send(message)
|
||||||
|
.map_err(|e| {
|
||||||
|
napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}"))
|
||||||
|
})
|
||||||
|
// NAPI doesn't support u64 or usize, so we need to convert to u32
|
||||||
|
.map(|u| u32::try_from(u).unwrap_or_default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
19
apps/desktop/desktop_native/proxy/Cargo.toml
Normal file
19
apps/desktop/desktop_native/proxy/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
edition = "2021"
|
||||||
|
exclude = ["index.node"]
|
||||||
|
license = "GPL-3.0"
|
||||||
|
name = "desktop_proxy"
|
||||||
|
version = "0.0.0"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "=1.0.86"
|
||||||
|
desktop_core = { path = "../core", default-features = false }
|
||||||
|
futures = "0.3.30"
|
||||||
|
log = "0.4.21"
|
||||||
|
simplelog = "0.12.2"
|
||||||
|
tokio = { version = "1.38.0", features = ["io-std", "io-util", "macros", "rt"] }
|
||||||
|
tokio-util = { version = "0.7.11", features = ["codec"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
embed_plist = "1.2.2"
|
159
apps/desktop/desktop_native/proxy/src/main.rs
Normal file
159
apps/desktop/desktop_native/proxy/src/main.rs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use desktop_core::ipc::{MESSAGE_CHANNEL_BUFFER, NATIVE_MESSAGING_BUFFER_SIZE};
|
||||||
|
use futures::{FutureExt, SinkExt, StreamExt};
|
||||||
|
use log::*;
|
||||||
|
use tokio_util::codec::LengthDelimitedCodec;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
embed_plist::embed_info_plist!("../../../resources/info.desktop_proxy.plist");
|
||||||
|
|
||||||
|
fn init_logging(log_path: &Path, level: log::LevelFilter) {
|
||||||
|
use simplelog::{ColorChoice, CombinedLogger, Config, SharedLogger, TermLogger, TerminalMode};
|
||||||
|
|
||||||
|
let config = Config::default();
|
||||||
|
|
||||||
|
let mut loggers: Vec<Box<dyn SharedLogger>> = Vec::new();
|
||||||
|
loggers.push(TermLogger::new(
|
||||||
|
level,
|
||||||
|
config.clone(),
|
||||||
|
TerminalMode::Stderr,
|
||||||
|
ColorChoice::Auto,
|
||||||
|
));
|
||||||
|
|
||||||
|
match std::fs::File::create(log_path) {
|
||||||
|
Ok(file) => {
|
||||||
|
loggers.push(simplelog::WriteLogger::new(level, config, file));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Can't create file: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = CombinedLogger::init(loggers) {
|
||||||
|
eprintln!("Failed to initialize logger: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bitwarden IPC Proxy.
|
||||||
|
///
|
||||||
|
/// This proxy allows browser extensions to communicate with a desktop application using Native
|
||||||
|
/// Messaging. This method allows an extension to send and receive messages through the use of
|
||||||
|
/// stdin/stdout streams.
|
||||||
|
///
|
||||||
|
/// However, this also requires the browser to start the process in order for the communication to
|
||||||
|
/// occur. To overcome this limitation, we implement Inter-Process Communication (IPC) to establish
|
||||||
|
/// a stable communication channel between the proxy and the running desktop application.
|
||||||
|
///
|
||||||
|
/// Browser extension <-[native messaging]-> proxy <-[ipc]-> desktop
|
||||||
|
///
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() {
|
||||||
|
let sock_path = desktop_core::ipc::path("bitwarden");
|
||||||
|
|
||||||
|
let log_path = {
|
||||||
|
let mut path = sock_path.clone();
|
||||||
|
path.set_extension("bitwarden.log");
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
init_logging(&log_path, LevelFilter::Info);
|
||||||
|
|
||||||
|
info!("Starting Bitwarden IPC Proxy.");
|
||||||
|
|
||||||
|
// Different browsers send different arguments when the app starts:
|
||||||
|
//
|
||||||
|
// Firefox:
|
||||||
|
// - The complete path to the app manifest. (in the form `/Users/<user>/Library/.../Mozilla/NativeMessagingHosts/com.8bit.bitwarden.json`)
|
||||||
|
// - (in Firefox 55+) the ID (as given in the manifest.json) of the add-on that started it (in the form `{[UUID]}`).
|
||||||
|
//
|
||||||
|
// Chrome on Windows:
|
||||||
|
// - Origin of the extension that started it (in the form `chrome-extension://[ID]`).
|
||||||
|
// - Handle to the Chrome native window that started the app.
|
||||||
|
//
|
||||||
|
// Chrome on Linux and Mac:
|
||||||
|
// - Origin of the extension that started it (in the form `chrome-extension://[ID]`).
|
||||||
|
|
||||||
|
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||||
|
info!("Process args: {:?}", args);
|
||||||
|
|
||||||
|
// Setup two channels, one for sending messages to the desktop application (`out`) and one for receiving messages from the desktop application (`in`)
|
||||||
|
let (in_send, in_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER);
|
||||||
|
let (out_send, mut out_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER);
|
||||||
|
|
||||||
|
let mut handle = tokio::spawn(
|
||||||
|
desktop_core::ipc::client::connect(sock_path, out_send, in_recv)
|
||||||
|
.map(|r| r.map_err(|e| e.to_string())),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a new codec for reading and writing messages from stdin/stdout.
|
||||||
|
let mut stdin = LengthDelimitedCodec::builder()
|
||||||
|
.max_frame_length(NATIVE_MESSAGING_BUFFER_SIZE)
|
||||||
|
.native_endian()
|
||||||
|
.new_read(tokio::io::stdin());
|
||||||
|
let mut stdout = LengthDelimitedCodec::builder()
|
||||||
|
.max_frame_length(NATIVE_MESSAGING_BUFFER_SIZE)
|
||||||
|
.native_endian()
|
||||||
|
.new_write(tokio::io::stdout());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
// This forces tokio to poll the futures in the order that they are written.
|
||||||
|
// We want the spawn handle to be evaluated first so that we can get any error
|
||||||
|
// results before we get the channel closed message.
|
||||||
|
biased;
|
||||||
|
|
||||||
|
// IPC client has finished, so we should exit as well.
|
||||||
|
res = &mut handle => {
|
||||||
|
match res {
|
||||||
|
Ok(Ok(())) => {
|
||||||
|
info!("IPC client finished successfully.");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
error!("IPC client connection error: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("IPC client spawn error: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive messages from IPC and print to STDOUT.
|
||||||
|
msg = out_recv.recv() => {
|
||||||
|
match msg {
|
||||||
|
Some(msg) => {
|
||||||
|
debug!("OUT: {}", msg);
|
||||||
|
stdout.send(msg.into()).await.unwrap();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
info!("Channel closed, exiting.");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Listen to stdin and send messages to ipc processor.
|
||||||
|
msg = stdin.next() => {
|
||||||
|
match msg {
|
||||||
|
Some(Ok(msg)) => {
|
||||||
|
let m = String::from_utf8(msg.to_vec()).unwrap();
|
||||||
|
debug!("IN: {}", m);
|
||||||
|
in_send.send(m).await.unwrap();
|
||||||
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
error!("Error parsing input: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
info!("Received EOF, exiting.");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -73,6 +73,13 @@
|
|||||||
"CFBundleDevelopmentRegion": "en"
|
"CFBundleDevelopmentRegion": "en"
|
||||||
},
|
},
|
||||||
"singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node",
|
"singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node",
|
||||||
|
"extraFiles": [
|
||||||
|
{
|
||||||
|
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}",
|
||||||
|
"to": "MacOS/desktop_proxy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"signIgnore": ["MacOS/desktop_proxy"],
|
||||||
"target": ["dmg", "zip"]
|
"target": ["dmg", "zip"]
|
||||||
},
|
},
|
||||||
"win": {
|
"win": {
|
||||||
@ -84,16 +91,24 @@
|
|||||||
"from": "../../node_modules/regedit/vbs",
|
"from": "../../node_modules/regedit/vbs",
|
||||||
"to": "regedit/vbs",
|
"to": "regedit/vbs",
|
||||||
"filter": ["**/*"]
|
"filter": ["**/*"]
|
||||||
},
|
}
|
||||||
|
],
|
||||||
|
"extraFiles": [
|
||||||
{
|
{
|
||||||
"from": "resources/native-messaging.bat",
|
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe",
|
||||||
"to": "native-messaging.bat"
|
"to": "desktop_proxy.exe"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"category": "Utility",
|
"category": "Utility",
|
||||||
"synopsis": "A secure and free password manager for all of your devices.",
|
"synopsis": "A secure and free password manager for all of your devices.",
|
||||||
|
"extraFiles": [
|
||||||
|
{
|
||||||
|
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}",
|
||||||
|
"to": "desktop_proxy"
|
||||||
|
}
|
||||||
|
],
|
||||||
"target": ["deb", "freebsd", "rpm", "AppImage", "snap"],
|
"target": ["deb", "freebsd", "rpm", "AppImage", "snap"],
|
||||||
"desktop": {
|
"desktop": {
|
||||||
"Name": "Bitwarden",
|
"Name": "Bitwarden",
|
||||||
|
@ -46,6 +46,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@cspotcode/source-map-support": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/trace-mapping": "0.3.9"
|
"@jridgewell/trace-mapping": "0.3.9"
|
||||||
@ -56,17 +58,23 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.4.15",
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.9",
|
"version": "0.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||||
|
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.0.3",
|
"@jridgewell/resolve-uri": "^3.0.3",
|
||||||
@ -74,19 +82,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@tsconfig/node12": {
|
"node_modules/@tsconfig/node12": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@tsconfig/node14": {
|
"node_modules/@tsconfig/node14": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@tsconfig/node16": {
|
"node_modules/@tsconfig/node16": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
@ -100,6 +116,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@types/node-ipc": {
|
"node_modules/@types/node-ipc": {
|
||||||
"version": "9.2.3",
|
"version": "9.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz",
|
||||||
|
"integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -107,7 +125,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.11.3",
|
"version": "8.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||||
|
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
@ -117,14 +137,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn-walk": {
|
"node_modules/acorn-walk": {
|
||||||
"version": "8.3.2",
|
"version": "8.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||||
|
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.0"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@ -132,6 +159,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/ansi-styles": {
|
"node_modules/ansi-styles": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-convert": "^2.0.1"
|
"color-convert": "^2.0.1"
|
||||||
@ -145,10 +174,14 @@
|
|||||||
},
|
},
|
||||||
"node_modules/arg": {
|
"node_modules/arg": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"string-width": "^4.2.0",
|
"string-width": "^4.2.0",
|
||||||
@ -161,6 +194,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-name": "~1.1.4"
|
"color-name": "~1.1.4"
|
||||||
@ -171,14 +206,20 @@
|
|||||||
},
|
},
|
||||||
"node_modules/color-name": {
|
"node_modules/color-name": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/create-require": {
|
"node_modules/create-require": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/diff": {
|
"node_modules/diff": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.3.1"
|
"node": ">=0.3.1"
|
||||||
@ -186,6 +227,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/easy-stack": {
|
"node_modules/easy-stack": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@ -193,10 +236,14 @@
|
|||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.1.2",
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@ -204,6 +251,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/event-pubsub": {
|
"node_modules/event-pubsub": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0.0"
|
"node": ">=4.0.0"
|
||||||
@ -211,6 +260,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/get-caller-file": {
|
"node_modules/get-caller-file": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "6.* || 8.* || >= 10.*"
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
@ -218,6 +269,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@ -225,6 +278,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/js-message": {
|
"node_modules/js-message": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.6.0"
|
"node": ">=0.6.0"
|
||||||
@ -232,6 +287,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/js-queue": {
|
"node_modules/js-queue": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"easy-stack": "^1.0.1"
|
"easy-stack": "^1.0.1"
|
||||||
@ -242,14 +299,20 @@
|
|||||||
},
|
},
|
||||||
"node_modules/make-error": {
|
"node_modules/make-error": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/module-alias": {
|
"node_modules/module-alias": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-ipc": {
|
"node_modules/node-ipc": {
|
||||||
"version": "9.2.1",
|
"version": "9.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz",
|
||||||
|
"integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"event-pubsub": "4.3.0",
|
"event-pubsub": "4.3.0",
|
||||||
@ -262,6 +325,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@ -269,6 +334,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
@ -281,6 +348,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/strip-ansi": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
@ -334,6 +403,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "4.7.4",
|
"version": "4.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||||
|
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@ -364,10 +435,14 @@
|
|||||||
},
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.0.0",
|
"ansi-styles": "^4.0.0",
|
||||||
@ -383,6 +458,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@ -390,6 +467,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cliui": "^8.0.1",
|
"cliui": "^8.0.1",
|
||||||
@ -406,6 +485,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/yargs-parser": {
|
"node_modules/yargs-parser": {
|
||||||
"version": "21.1.1",
|
"version": "21.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@ -413,6 +494,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/yn": {
|
"node_modules/yn": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "electron-rebuild",
|
"postinstall": "electron-rebuild",
|
||||||
"start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build",
|
"start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build",
|
||||||
"build-native": "cd desktop_native/napi && npm run build",
|
"build-native": "cd desktop_native && node build.js",
|
||||||
"build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"",
|
"build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"",
|
||||||
"build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"",
|
"build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"",
|
||||||
"build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js",
|
"build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js",
|
||||||
|
12
apps/desktop/resources/entitlements.desktop_proxy.plist
Normal file
12
apps/desktop/resources/entitlements.desktop_proxy.plist
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -8,6 +8,10 @@
|
|||||||
<string>LTZ2PFU5D6</string>
|
<string>LTZ2PFU5D6</string>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||||
|
</array>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
8
apps/desktop/resources/info.desktop_proxy.plist
Normal file
8
apps/desktop/resources/info.desktop_proxy.plist
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.bitwarden.desktop</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -1,7 +0,0 @@
|
|||||||
@echo off
|
|
||||||
:: Helper script for starting the Native Messaging Proxy on Windows.
|
|
||||||
|
|
||||||
cd ../
|
|
||||||
set ELECTRON_RUN_AS_NODE=1
|
|
||||||
set ELECTRON_NO_ATTACH_CONSOLE=1
|
|
||||||
Bitwarden.exe resources/app.asar %*
|
|
@ -1,14 +1,22 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
|
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
const child_process = require("child_process");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
|
const { flipFuses, FuseVersion, FuseV1Options } = require("@electron/fuses");
|
||||||
|
const builder = require("electron-builder");
|
||||||
const fse = require("fs-extra");
|
const fse = require("fs-extra");
|
||||||
|
|
||||||
exports.default = run;
|
exports.default = run;
|
||||||
|
|
||||||
async function run(context) {
|
async function run(context) {
|
||||||
console.log("## After pack");
|
console.log("## After pack");
|
||||||
console.log(context);
|
// console.log(context);
|
||||||
|
|
||||||
|
if (context.packager.platform.nodeName !== "darwin" || context.arch === builder.Arch.universal) {
|
||||||
|
await addElectronFuses(context);
|
||||||
|
}
|
||||||
|
|
||||||
if (context.electronPlatformName === "linux") {
|
if (context.electronPlatformName === "linux") {
|
||||||
console.log("Creating memory-protection wrapper script");
|
console.log("Creating memory-protection wrapper script");
|
||||||
const appOutDir = context.appOutDir;
|
const appOutDir = context.appOutDir;
|
||||||
@ -23,4 +31,132 @@ async function run(context) {
|
|||||||
fse.chmodSync(wrapperBin, "755");
|
fse.chmodSync(wrapperBin, "755");
|
||||||
console.log("Copied memory-protection wrapper script");
|
console.log("Copied memory-protection wrapper script");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (["darwin", "mas"].includes(context.electronPlatformName)) {
|
||||||
|
const is_mas = context.electronPlatformName === "mas";
|
||||||
|
const is_mas_dev = context.targets.some((e) => e.name === "mas-dev");
|
||||||
|
|
||||||
|
let id;
|
||||||
|
|
||||||
|
// Only use the Bitwarden Identities on CI
|
||||||
|
if (process.env.GITHUB_ACTIONS === "true") {
|
||||||
|
if (is_mas) {
|
||||||
|
id = is_mas_dev
|
||||||
|
? "E7C9978F6FBCE0553429185C405E61F5380BE8EB"
|
||||||
|
: "3rd Party Mac Developer Application: Bitwarden Inc";
|
||||||
|
} else {
|
||||||
|
id = "Developer ID Application: 8bit Solutions LLC";
|
||||||
|
}
|
||||||
|
// Locally, use the first valid code signing identity, unless CSC_NAME is set
|
||||||
|
} else if (process.env.CSC_NAME) {
|
||||||
|
id = process.env.CSC_NAME;
|
||||||
|
} else {
|
||||||
|
const identities = getIdentities();
|
||||||
|
if (identities.length === 0) {
|
||||||
|
throw new Error("No valid identities found");
|
||||||
|
}
|
||||||
|
id = identities[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Signing proxy binary before the main bundle, using identity '${id}'`);
|
||||||
|
|
||||||
|
const appName = context.packager.appInfo.productFilename;
|
||||||
|
const appPath = `${context.appOutDir}/${appName}.app`;
|
||||||
|
const proxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy");
|
||||||
|
|
||||||
|
const packageId = "com.bitwarden.desktop";
|
||||||
|
const entitlementsName = "entitlements.desktop_proxy.plist";
|
||||||
|
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
|
||||||
|
child_process.execSync(
|
||||||
|
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partially based on electron-builder code:
|
||||||
|
// https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/macPackager.ts
|
||||||
|
// https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/codeSign/macCodeSign.ts
|
||||||
|
|
||||||
|
const appleCertificatePrefixes = [
|
||||||
|
"Developer ID Application:",
|
||||||
|
// "Developer ID Installer:",
|
||||||
|
// "3rd Party Mac Developer Application:",
|
||||||
|
// "3rd Party Mac Developer Installer:",
|
||||||
|
"Apple Development:",
|
||||||
|
];
|
||||||
|
|
||||||
|
function getIdentities() {
|
||||||
|
const ids = child_process
|
||||||
|
.execSync("/usr/bin/security find-identity -v -p codesigning")
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
return ids
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => {
|
||||||
|
for (const prefix of appleCertificatePrefixes) {
|
||||||
|
if (line.includes(prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.map((line) => {
|
||||||
|
const split = line.trim().split(" ");
|
||||||
|
const id = split[1];
|
||||||
|
const name = split.slice(2).join(" ").replace(/"/g, "");
|
||||||
|
return { id, name };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("electron-builder").AfterPackContext} context
|
||||||
|
*/
|
||||||
|
async function addElectronFuses(context) {
|
||||||
|
const platform = context.packager.platform.nodeName;
|
||||||
|
|
||||||
|
const ext = {
|
||||||
|
darwin: ".app",
|
||||||
|
win32: ".exe",
|
||||||
|
linux: "",
|
||||||
|
}[platform];
|
||||||
|
|
||||||
|
const IS_LINUX = platform === "linux";
|
||||||
|
const executableName = IS_LINUX
|
||||||
|
? context.packager.appInfo.productFilename.toLowerCase().replace("-dev", "").replace(" ", "-")
|
||||||
|
: context.packager.appInfo.productFilename; // .toLowerCase() to accomodate Linux file named `name` but productFileName is `Name` -- Replaces '-dev' because on Linux the executable name is `name` even for the DEV builds
|
||||||
|
|
||||||
|
const electronBinaryPath = path.join(context.appOutDir, `${executableName}${ext}`);
|
||||||
|
|
||||||
|
console.log("## Adding fuses to the electron binary", electronBinaryPath);
|
||||||
|
|
||||||
|
await flipFuses(electronBinaryPath, {
|
||||||
|
version: FuseVersion.V1,
|
||||||
|
strictlyRequireAllFuses: true,
|
||||||
|
resetAdHocDarwinSignature: platform === "darwin" && context.arch === builder.Arch.universal,
|
||||||
|
|
||||||
|
// List of fuses and their default values is available at:
|
||||||
|
// https://www.electronjs.org/docs/latest/tutorial/fuses
|
||||||
|
|
||||||
|
[FuseV1Options.RunAsNode]: false,
|
||||||
|
[FuseV1Options.EnableCookieEncryption]: true,
|
||||||
|
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||||
|
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||||
|
|
||||||
|
// Currently, asar integrity is only implemented for macOS and Windows
|
||||||
|
// https://www.electronjs.org/docs/latest/tutorial/asar-integrity
|
||||||
|
// On macOS, it works by default, but on Windows it requires the
|
||||||
|
// asarIntegrity feature of electron-builder v25, currently in alpha
|
||||||
|
// https://github.com/electron-userland/electron-builder/releases/tag/v25.0.0-alpha.10
|
||||||
|
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: platform === "darwin",
|
||||||
|
|
||||||
|
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||||
|
|
||||||
|
// App refuses to open when enabled
|
||||||
|
[FuseV1Options.LoadBrowserProcessSpecificV8Snapshot]: false,
|
||||||
|
|
||||||
|
// To disable this, we should stop using the file:// protocol to load the app bundle
|
||||||
|
// This can be done by defining a custom app:// protocol and loading the bundle from there,
|
||||||
|
// but then any requests to the server will be blocked by CORS policy
|
||||||
|
[FuseV1Options.GrantFileProtocolExtraPrivileges]: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -11,11 +11,14 @@ import {
|
|||||||
unauthGuardFn,
|
unauthGuardFn,
|
||||||
} from "@bitwarden/angular/auth/guards";
|
} from "@bitwarden/angular/auth/guards";
|
||||||
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||||
|
import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect";
|
||||||
import {
|
import {
|
||||||
AnonLayoutWrapperComponent,
|
AnonLayoutWrapperComponent,
|
||||||
AnonLayoutWrapperData,
|
AnonLayoutWrapperData,
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
LoginSecondaryContentComponent,
|
LoginSecondaryContentComponent,
|
||||||
|
LockIcon,
|
||||||
|
LockV2Component,
|
||||||
PasswordHintComponent,
|
PasswordHintComponent,
|
||||||
RegistrationFinishComponent,
|
RegistrationFinishComponent,
|
||||||
RegistrationStartComponent,
|
RegistrationStartComponent,
|
||||||
@ -64,6 +67,7 @@ const routes: Routes = [
|
|||||||
path: "lock",
|
path: "lock",
|
||||||
component: LockComponent,
|
component: LockComponent,
|
||||||
canActivate: [lockGuard()],
|
canActivate: [lockGuard()],
|
||||||
|
canMatch: [extensionRefreshRedirect("/lockV2")],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "login-with-device",
|
path: "login-with-device",
|
||||||
@ -221,6 +225,21 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "lockV2",
|
||||||
|
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()],
|
||||||
|
data: {
|
||||||
|
pageIcon: LockIcon,
|
||||||
|
pageTitle: "yourVaultIsLockedV2",
|
||||||
|
showReadonlyHostname: true,
|
||||||
|
} satisfies AnonLayoutWrapperData,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: LockV2Component,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "set-password-jit",
|
path: "set-password-jit",
|
||||||
canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],
|
canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],
|
||||||
|
@ -19,7 +19,11 @@ import {
|
|||||||
CLIENT_TYPE,
|
CLIENT_TYPE,
|
||||||
} from "@bitwarden/angular/services/injection-tokens";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { LoginComponentService, SetPasswordJitService } from "@bitwarden/auth/angular";
|
import {
|
||||||
|
LoginComponentService,
|
||||||
|
SetPasswordJitService,
|
||||||
|
LockComponentService,
|
||||||
|
} from "@bitwarden/auth/angular";
|
||||||
import {
|
import {
|
||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
LoginEmailService,
|
LoginEmailService,
|
||||||
@ -96,6 +100,7 @@ import { ElectronRendererStorageService } from "../../platform/services/electron
|
|||||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||||
import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging";
|
import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging";
|
||||||
import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme";
|
import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme";
|
||||||
|
import { DesktopLockComponentService } from "../../services/desktop-lock-component.service";
|
||||||
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
|
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
|
||||||
import { NativeMessageHandlerService } from "../../services/native-message-handler.service";
|
import { NativeMessageHandlerService } from "../../services/native-message-handler.service";
|
||||||
import { NativeMessagingService } from "../../services/native-messaging.service";
|
import { NativeMessagingService } from "../../services/native-messaging.service";
|
||||||
@ -287,6 +292,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: NativeMessagingManifestService,
|
useClass: NativeMessagingManifestService,
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: LockComponentService,
|
||||||
|
useClass: DesktopLockComponentService,
|
||||||
|
deps: [],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: CLIENT_TYPE,
|
provide: CLIENT_TYPE,
|
||||||
useValue: ClientType.Desktop,
|
useValue: ClientType.Desktop,
|
||||||
@ -297,6 +307,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
deps: [
|
deps: [
|
||||||
ApiService,
|
ApiService,
|
||||||
CryptoService,
|
CryptoService,
|
||||||
|
EncryptService,
|
||||||
I18nServiceAbstraction,
|
I18nServiceAbstraction,
|
||||||
KdfConfigService,
|
KdfConfigService,
|
||||||
InternalMasterPasswordServiceAbstraction,
|
InternalMasterPasswordServiceAbstraction,
|
||||||
|
@ -8,7 +8,9 @@
|
|||||||
attr.aria-hidden="{{ showingModal }}"
|
attr.aria-hidden="{{ showingModal }}"
|
||||||
>
|
>
|
||||||
<div id="content" class="content" style="padding-top: 50px">
|
<div id="content" class="content" style="padding-top: 50px">
|
||||||
<img class="logo-image" alt="Bitwarden" />
|
<a (click)="invalidateEmail()" class="tw-cursor-pointer">
|
||||||
|
<img class="logo-image" alt="Bitwarden" />
|
||||||
|
</a>
|
||||||
<p class="lead">{{ "loginOrCreateNewAccount" | i18n }}</p>
|
<p class="lead">{{ "loginOrCreateNewAccount" | i18n }}</p>
|
||||||
<!-- start email -->
|
<!-- start email -->
|
||||||
<ng-container *ngIf="!validatedEmail; else loginPage">
|
<ng-container *ngIf="!validatedEmail; else loginPage">
|
||||||
|
@ -227,4 +227,11 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force the validatedEmail flag to false, which will show the login page.
|
||||||
|
*/
|
||||||
|
invalidateEmail() {
|
||||||
|
this.validatedEmail = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,33 @@
|
|||||||
import { NativeMessagingProxy } from "./proxy/native-messaging-proxy";
|
import { spawn } from "child_process";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
// We need to import the other dependencies using `require` since `import` will
|
import { app } from "electron";
|
||||||
// generate `Error: Cannot find module 'electron'`. The cause of this error is
|
|
||||||
// due to native messaging setting the ELECTRON_RUN_AS_NODE env flag on windows
|
|
||||||
// which removes the electron module. This flag is needed for stdin/out to work
|
|
||||||
// properly on Windows.
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
process.platform === "darwin" &&
|
||||||
process.argv.some((arg) => arg.indexOf("chrome-extension://") !== -1 || arg.indexOf("{") !== -1)
|
process.argv.some((arg) => arg.indexOf("chrome-extension://") !== -1 || arg.indexOf("{") !== -1)
|
||||||
) {
|
) {
|
||||||
if (process.platform === "darwin") {
|
// If we're on MacOS, we need to support DuckDuckGo's IPC communication,
|
||||||
// eslint-disable-next-line
|
// which for the moment is launching the Bitwarden process.
|
||||||
const app = require("electron").app;
|
// Ideally the browser would instead startup the desktop_proxy process
|
||||||
|
// when available, but for now we'll just launch it here.
|
||||||
|
|
||||||
app.on("ready", () => {
|
app.on("ready", () => {
|
||||||
app.dock.hide();
|
app.dock.hide();
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdout.on("error", (e) => {
|
|
||||||
if (e.code === "EPIPE") {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const proxy = new NativeMessagingProxy();
|
const proc = spawn(path.join(process.execPath, "..", "desktop_proxy"), process.argv.slice(1), {
|
||||||
proxy.run();
|
cwd: process.cwd(),
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("exit", () => {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
proc.on("error", () => {
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const Main = require("./main").Main;
|
const Main = require("./main").Main;
|
||||||
|
@ -939,6 +939,18 @@
|
|||||||
"yourVaultIsLocked": {
|
"yourVaultIsLocked": {
|
||||||
"message": "Your vault is locked. Verify your identity to continue."
|
"message": "Your vault is locked. Verify your identity to continue."
|
||||||
},
|
},
|
||||||
|
"yourAccountIsLocked": {
|
||||||
|
"message": "Your account is locked"
|
||||||
|
},
|
||||||
|
"or": {
|
||||||
|
"message": "or"
|
||||||
|
},
|
||||||
|
"unlockWithBiometrics": {
|
||||||
|
"message": "Unlock with biometrics"
|
||||||
|
},
|
||||||
|
"unlockWithMasterPassword": {
|
||||||
|
"message": "Unlock with master password"
|
||||||
|
},
|
||||||
"unlock": {
|
"unlock": {
|
||||||
"message": "Unlock"
|
"message": "Unlock"
|
||||||
},
|
},
|
||||||
@ -2277,6 +2289,9 @@
|
|||||||
"locked": {
|
"locked": {
|
||||||
"message": "Locked"
|
"message": "Locked"
|
||||||
},
|
},
|
||||||
|
"yourVaultIsLockedV2": {
|
||||||
|
"message": "Your vault is locked"
|
||||||
|
},
|
||||||
"unlocked": {
|
"unlocked": {
|
||||||
"message": "Unlocked"
|
"message": "Unlocked"
|
||||||
},
|
},
|
||||||
@ -2629,6 +2644,9 @@
|
|||||||
"important": {
|
"important": {
|
||||||
"message": "Important:"
|
"message": "Important:"
|
||||||
},
|
},
|
||||||
|
"accessing": {
|
||||||
|
"message": "Accessing"
|
||||||
|
},
|
||||||
"accessTokenUnableToBeDecrypted": {
|
"accessTokenUnableToBeDecrypted": {
|
||||||
"message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue."
|
"message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue."
|
||||||
},
|
},
|
||||||
|
@ -227,6 +227,7 @@ export class Main {
|
|||||||
this.windowMain,
|
this.windowMain,
|
||||||
app.getPath("userData"),
|
app.getPath("userData"),
|
||||||
app.getPath("exe"),
|
app.getPath("exe"),
|
||||||
|
app.getAppPath(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider);
|
this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider);
|
||||||
@ -273,13 +274,21 @@ export class Main {
|
|||||||
if (browserIntegrationEnabled || ddgIntegrationEnabled) {
|
if (browserIntegrationEnabled || ddgIntegrationEnabled) {
|
||||||
// Re-register the native messaging host integrations on startup, in case they are not present
|
// Re-register the native messaging host integrations on startup, in case they are not present
|
||||||
if (browserIntegrationEnabled) {
|
if (browserIntegrationEnabled) {
|
||||||
this.nativeMessagingMain.generateManifests().catch(this.logService.error);
|
this.nativeMessagingMain
|
||||||
|
.generateManifests()
|
||||||
|
.catch((err) => this.logService.error("Error while generating manifests", err));
|
||||||
}
|
}
|
||||||
if (ddgIntegrationEnabled) {
|
if (ddgIntegrationEnabled) {
|
||||||
this.nativeMessagingMain.generateDdgManifests().catch(this.logService.error);
|
this.nativeMessagingMain
|
||||||
|
.generateDdgManifests()
|
||||||
|
.catch((err) => this.logService.error("Error while generating DDG manifests", err));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.nativeMessagingMain.listen();
|
this.nativeMessagingMain
|
||||||
|
.listen()
|
||||||
|
.catch((err) =>
|
||||||
|
this.logService.error("Error while starting native message listener", err),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.removeAsDefaultProtocolClient("bitwarden");
|
app.removeAsDefaultProtocolClient("bitwarden");
|
||||||
|
@ -1,34 +1,34 @@
|
|||||||
import { existsSync, promises as fs } from "fs";
|
import { existsSync, promises as fs } from "fs";
|
||||||
import { Socket } from "net";
|
|
||||||
import { homedir, userInfo } from "os";
|
import { homedir, userInfo } from "os";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
|
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import * as ipc from "node-ipc";
|
|
||||||
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { ipc } from "@bitwarden/desktop-napi";
|
||||||
|
|
||||||
import { getIpcSocketRoot } from "../proxy/ipc";
|
import { isDev } from "../utils";
|
||||||
|
|
||||||
import { WindowMain } from "./window.main";
|
import { WindowMain } from "./window.main";
|
||||||
|
|
||||||
export class NativeMessagingMain {
|
export class NativeMessagingMain {
|
||||||
private connected: Socket[] = [];
|
private ipcServer: ipc.IpcServer | null;
|
||||||
private socket: any;
|
private connected: number[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private windowMain: WindowMain,
|
private windowMain: WindowMain,
|
||||||
private userPath: string,
|
private userPath: string,
|
||||||
private exePath: string,
|
private exePath: string,
|
||||||
|
private appPath: string,
|
||||||
) {
|
) {
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"nativeMessaging.manifests",
|
"nativeMessaging.manifests",
|
||||||
async (_event: any, options: { create: boolean }) => {
|
async (_event: any, options: { create: boolean }) => {
|
||||||
if (options.create) {
|
if (options.create) {
|
||||||
this.listen();
|
|
||||||
try {
|
try {
|
||||||
|
await this.listen();
|
||||||
await this.generateManifests();
|
await this.generateManifests();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error("Error generating manifests: " + e);
|
this.logService.error("Error generating manifests: " + e);
|
||||||
@ -51,8 +51,8 @@ export class NativeMessagingMain {
|
|||||||
"nativeMessaging.ddgManifests",
|
"nativeMessaging.ddgManifests",
|
||||||
async (_event: any, options: { create: boolean }) => {
|
async (_event: any, options: { create: boolean }) => {
|
||||||
if (options.create) {
|
if (options.create) {
|
||||||
this.listen();
|
|
||||||
try {
|
try {
|
||||||
|
await this.listen();
|
||||||
await this.generateDdgManifests();
|
await this.generateDdgManifests();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error("Error generating duckduckgo manifests: " + e);
|
this.logService.error("Error generating duckduckgo manifests: " + e);
|
||||||
@ -72,56 +72,46 @@ export class NativeMessagingMain {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
listen() {
|
async listen() {
|
||||||
ipc.config.id = "bitwarden";
|
if (this.ipcServer) {
|
||||||
ipc.config.retry = 1500;
|
this.ipcServer.stop();
|
||||||
const ipcSocketRoot = getIpcSocketRoot();
|
|
||||||
if (ipcSocketRoot != null) {
|
|
||||||
ipc.config.socketRoot = ipcSocketRoot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ipc.serve(() => {
|
this.ipcServer = await ipc.IpcServer.listen("bitwarden", (error, msg) => {
|
||||||
ipc.server.on("message", (data: any, socket: any) => {
|
switch (msg.kind) {
|
||||||
this.socket = socket;
|
case ipc.IpcMessageType.Connected: {
|
||||||
this.windowMain.win.webContents.send("nativeMessaging", data);
|
this.connected.push(msg.clientId);
|
||||||
});
|
this.logService.info("Native messaging client " + msg.clientId + " has connected");
|
||||||
|
break;
|
||||||
ipcMain.on("nativeMessagingReply", (event, msg) => {
|
|
||||||
if (this.socket != null && msg != null) {
|
|
||||||
this.send(msg, this.socket);
|
|
||||||
}
|
}
|
||||||
});
|
case ipc.IpcMessageType.Disconnected: {
|
||||||
|
const index = this.connected.indexOf(msg.clientId);
|
||||||
|
if (index > -1) {
|
||||||
|
this.connected.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
ipc.server.on("connect", (socket: Socket) => {
|
this.logService.info("Native messaging client " + msg.clientId + " has disconnected");
|
||||||
this.connected.push(socket);
|
break;
|
||||||
});
|
|
||||||
|
|
||||||
ipc.server.on("socket.disconnected", (socket, destroyedSocketID) => {
|
|
||||||
const index = this.connected.indexOf(socket);
|
|
||||||
if (index > -1) {
|
|
||||||
this.connected.splice(index, 1);
|
|
||||||
}
|
}
|
||||||
|
case ipc.IpcMessageType.Message:
|
||||||
this.socket = null;
|
this.windowMain.win.webContents.send("nativeMessaging", JSON.parse(msg.message));
|
||||||
ipc.log("client " + destroyedSocketID + " has disconnected!");
|
break;
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.server.start();
|
ipcMain.on("nativeMessagingReply", (event, msg) => {
|
||||||
}
|
if (msg != null) {
|
||||||
|
this.send(msg);
|
||||||
stop() {
|
|
||||||
ipc.server.stop();
|
|
||||||
// Kill all existing connections
|
|
||||||
this.connected.forEach((socket) => {
|
|
||||||
if (!socket.destroyed) {
|
|
||||||
socket.destroy();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
send(message: object, socket: any) {
|
stop() {
|
||||||
ipc.server.emit(socket, "message", message);
|
this.ipcServer?.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
send(message: object) {
|
||||||
|
this.ipcServer?.send(JSON.stringify(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateManifests() {
|
async generateManifests() {
|
||||||
@ -211,6 +201,13 @@ export class NativeMessagingMain {
|
|||||||
chromeJson,
|
chromeJson,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existsSync(`${this.homedir()}/.config/chromium/`)) {
|
||||||
|
await this.writeManifest(
|
||||||
|
`${this.homedir()}/.config/chromium/NativeMessagingHosts/com.8bit.bitwarden.json`,
|
||||||
|
chromeJson,
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@ -331,11 +328,20 @@ export class NativeMessagingMain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private binaryPath() {
|
private binaryPath() {
|
||||||
if (process.platform === "win32") {
|
const ext = process.platform === "win32" ? ".exe" : "";
|
||||||
return path.join(path.dirname(this.exePath), "resources", "native-messaging.bat");
|
|
||||||
|
if (isDev()) {
|
||||||
|
return path.join(
|
||||||
|
this.appPath,
|
||||||
|
"..",
|
||||||
|
"desktop_native",
|
||||||
|
"target",
|
||||||
|
"debug",
|
||||||
|
`desktop_proxy${ext}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.exePath;
|
return path.join(path.dirname(this.exePath), `desktop_proxy${ext}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRegeditInstance() {
|
private getRegeditInstance() {
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import { createHash } from "crypto";
|
|
||||||
import { existsSync, mkdirSync } from "fs";
|
|
||||||
import { homedir } from "os";
|
|
||||||
import { join as path_join } from "path";
|
|
||||||
|
|
||||||
import * as ipc from "node-ipc";
|
|
||||||
|
|
||||||
export function getIpcSocketRoot(): string | null {
|
|
||||||
let socketRoot = null;
|
|
||||||
|
|
||||||
switch (process.platform) {
|
|
||||||
case "darwin": {
|
|
||||||
const ipcSocketRootDir = path_join(homedir(), "tmp");
|
|
||||||
if (!existsSync(ipcSocketRootDir)) {
|
|
||||||
mkdirSync(ipcSocketRootDir);
|
|
||||||
}
|
|
||||||
socketRoot = ipcSocketRootDir + "/";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "win32": {
|
|
||||||
// Let node-ipc use a unique IPC pipe //./pipe/xxxxxxxxxxxxxxxxx.app.bitwarden per user.
|
|
||||||
// Hashing prevents problems with reserved characters and file length limitations.
|
|
||||||
socketRoot = createHash("sha1").update(homedir()).digest("hex") + ".";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return socketRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
ipc.config.id = "proxy";
|
|
||||||
ipc.config.retry = 1500;
|
|
||||||
ipc.config.logger = console.warn; // Stdout is used for native messaging
|
|
||||||
const ipcSocketRoot = getIpcSocketRoot();
|
|
||||||
if (ipcSocketRoot != null) {
|
|
||||||
ipc.config.socketRoot = ipcSocketRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class IPC {
|
|
||||||
onMessage: (message: object) => void;
|
|
||||||
|
|
||||||
private connected = false;
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
ipc.connectTo("bitwarden", () => {
|
|
||||||
ipc.of.bitwarden.on("connect", () => {
|
|
||||||
this.connected = true;
|
|
||||||
console.error("## connected to bitwarden desktop ##");
|
|
||||||
|
|
||||||
// Notify browser extension, connection is established to desktop application.
|
|
||||||
this.onMessage({ command: "connected" });
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.of.bitwarden.on("disconnect", () => {
|
|
||||||
this.connected = false;
|
|
||||||
console.error("disconnected from world");
|
|
||||||
|
|
||||||
// Notify browser extension, no connection to desktop application.
|
|
||||||
this.onMessage({ command: "disconnected" });
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.of.bitwarden.on("message", (message: any) => {
|
|
||||||
this.onMessage(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.of.bitwarden.on("error", (err: any) => {
|
|
||||||
console.error("error", err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isConnected(): boolean {
|
|
||||||
return this.connected;
|
|
||||||
}
|
|
||||||
|
|
||||||
send(json: object) {
|
|
||||||
ipc.of.bitwarden.emit("message", json);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
import IPC from "./ipc";
|
|
||||||
import NativeMessage from "./nativemessage";
|
|
||||||
|
|
||||||
// Proxy is a lightweight application which provides bi-directional communication
|
|
||||||
// between the browser extension and a running desktop application.
|
|
||||||
//
|
|
||||||
// Browser extension <-[native messaging]-> proxy <-[ipc]-> desktop
|
|
||||||
export class NativeMessagingProxy {
|
|
||||||
private ipc: IPC;
|
|
||||||
private nativeMessage: NativeMessage;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.ipc = new IPC();
|
|
||||||
this.nativeMessage = new NativeMessage(this.ipc);
|
|
||||||
}
|
|
||||||
|
|
||||||
run() {
|
|
||||||
this.ipc.connect();
|
|
||||||
this.nativeMessage.listen();
|
|
||||||
|
|
||||||
this.ipc.onMessage = this.nativeMessage.send;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import IPC from "./ipc";
|
|
||||||
|
|
||||||
// Mostly based on the example from MDN,
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging
|
|
||||||
export default class NativeMessage {
|
|
||||||
ipc: IPC;
|
|
||||||
|
|
||||||
constructor(ipc: IPC) {
|
|
||||||
this.ipc = ipc;
|
|
||||||
}
|
|
||||||
|
|
||||||
send(message: object) {
|
|
||||||
const messageBuffer = Buffer.from(JSON.stringify(message));
|
|
||||||
|
|
||||||
const headerBuffer = Buffer.alloc(4);
|
|
||||||
headerBuffer.writeUInt32LE(messageBuffer.length, 0);
|
|
||||||
|
|
||||||
process.stdout.write(Buffer.concat([headerBuffer, messageBuffer]));
|
|
||||||
}
|
|
||||||
|
|
||||||
listen() {
|
|
||||||
let payloadSize: number = null;
|
|
||||||
|
|
||||||
// A queue to store the chunks as we read them from stdin.
|
|
||||||
// This queue can be flushed when `payloadSize` data has been read
|
|
||||||
const chunks: any = [];
|
|
||||||
|
|
||||||
// Only read the size once for each payload
|
|
||||||
const sizeHasBeenRead = () => Boolean(payloadSize);
|
|
||||||
|
|
||||||
// All the data has been read, reset everything for the next message
|
|
||||||
const flushChunksQueue = () => {
|
|
||||||
payloadSize = null;
|
|
||||||
chunks.splice(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const processData = () => {
|
|
||||||
// Create one big buffer with all all the chunks
|
|
||||||
const stringData = Buffer.concat(chunks);
|
|
||||||
console.error(stringData);
|
|
||||||
|
|
||||||
// The browser will emit the size as a header of the payload,
|
|
||||||
// if it hasn't been read yet, do it.
|
|
||||||
// The next time we'll need to read the payload size is when all of the data
|
|
||||||
// of the current payload has been read (ie. data.length >= payloadSize + 4)
|
|
||||||
if (!sizeHasBeenRead()) {
|
|
||||||
try {
|
|
||||||
payloadSize = stringData.readUInt32LE(0);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the data we have read so far is >= to the size advertised in the header,
|
|
||||||
// it means we have all of the data sent.
|
|
||||||
// We add 4 here because that's the size of the bytes that old the payloadSize
|
|
||||||
if (stringData.length >= payloadSize + 4) {
|
|
||||||
// Remove the header
|
|
||||||
const contentWithoutSize = stringData.slice(4, payloadSize + 4).toString();
|
|
||||||
|
|
||||||
// Reset the read size and the queued chunks
|
|
||||||
flushChunksQueue();
|
|
||||||
|
|
||||||
const json = JSON.parse(contentWithoutSize);
|
|
||||||
|
|
||||||
// Forward to desktop application
|
|
||||||
this.ipc.send(json);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
process.stdin.on("readable", () => {
|
|
||||||
// A temporary variable holding the nodejs.Buffer of each
|
|
||||||
// chunk of data read off stdin
|
|
||||||
let chunk = null;
|
|
||||||
|
|
||||||
// Read all of the available data
|
|
||||||
// tslint:disable-next-line:no-conditional-assignment
|
|
||||||
while ((chunk = process.stdin.read()) !== null) {
|
|
||||||
chunks.push(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
processData();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
process.stdin.on("end", () => {
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
377
apps/desktop/src/services/desktop-lock-component.service.spec.ts
Normal file
377
apps/desktop/src/services/desktop-lock-component.service.spec.ts
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
|
import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/auth/angular";
|
||||||
|
import {
|
||||||
|
PinServiceAbstraction,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
} from "@bitwarden/auth/common";
|
||||||
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { BiometricsService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { DesktopLockComponentService } from "./desktop-lock-component.service";
|
||||||
|
|
||||||
|
// ipc mock global
|
||||||
|
const isWindowVisibleMock = jest.fn();
|
||||||
|
const biometricEnabledMock = jest.fn();
|
||||||
|
(global as any).ipc = {
|
||||||
|
keyManagement: {
|
||||||
|
biometric: {
|
||||||
|
enabled: biometricEnabledMock,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
platform: {
|
||||||
|
isWindowVisible: isWindowVisibleMock,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("DesktopLockComponentService", () => {
|
||||||
|
let service: DesktopLockComponentService;
|
||||||
|
|
||||||
|
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||||
|
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||||
|
let biometricsService: MockProxy<BiometricsService>;
|
||||||
|
let pinService: MockProxy<PinServiceAbstraction>;
|
||||||
|
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||||
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||||
|
platformUtilsService = mock<PlatformUtilsService>();
|
||||||
|
biometricsService = mock<BiometricsService>();
|
||||||
|
pinService = mock<PinServiceAbstraction>();
|
||||||
|
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||||
|
cryptoService = mock<CryptoService>();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
DesktopLockComponentService,
|
||||||
|
{
|
||||||
|
provide: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
useValue: userDecryptionOptionsService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PlatformUtilsService,
|
||||||
|
useValue: platformUtilsService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: BiometricsService,
|
||||||
|
useValue: biometricsService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PinServiceAbstraction,
|
||||||
|
useValue: pinService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: VaultTimeoutSettingsService,
|
||||||
|
useValue: vaultTimeoutSettingsService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: CryptoService,
|
||||||
|
useValue: cryptoService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(DesktopLockComponentService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("instantiates", () => {
|
||||||
|
expect(service).not.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// getBiometricsError
|
||||||
|
describe("getBiometricsError", () => {
|
||||||
|
it("returns null when given null", () => {
|
||||||
|
const result = service.getBiometricsError(null);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when given an unknown error", () => {
|
||||||
|
const result = service.getBiometricsError({ message: "unknown" });
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPreviousUrl", () => {
|
||||||
|
it("returns null", () => {
|
||||||
|
const result = service.getPreviousUrl();
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isWindowVisible", () => {
|
||||||
|
it("returns the window visibility", async () => {
|
||||||
|
isWindowVisibleMock.mockReturnValue(true);
|
||||||
|
const result = await service.isWindowVisible();
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getBiometricsUnlockBtnText", () => {
|
||||||
|
it("returns the correct text for Mac OS", () => {
|
||||||
|
platformUtilsService.getDevice.mockReturnValue(DeviceType.MacOsDesktop);
|
||||||
|
const result = service.getBiometricsUnlockBtnText();
|
||||||
|
expect(result).toBe("unlockWithTouchId");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the correct text for Windows", () => {
|
||||||
|
platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop);
|
||||||
|
const result = service.getBiometricsUnlockBtnText();
|
||||||
|
expect(result).toBe("unlockWithWindowsHello");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the correct text for Linux", () => {
|
||||||
|
platformUtilsService.getDevice.mockReturnValue(DeviceType.LinuxDesktop);
|
||||||
|
const result = service.getBiometricsUnlockBtnText();
|
||||||
|
expect(result).toBe("unlockWithPolkit");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error for an unsupported platform", () => {
|
||||||
|
platformUtilsService.getDevice.mockReturnValue("unsupported" as any);
|
||||||
|
expect(() => service.getBiometricsUnlockBtnText()).toThrowError("Unsupported platform");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAvailableUnlockOptions$", () => {
|
||||||
|
interface MockInputs {
|
||||||
|
hasMasterPassword: boolean;
|
||||||
|
osSupportsBiometric: boolean;
|
||||||
|
biometricLockSet: boolean;
|
||||||
|
biometricReady: boolean;
|
||||||
|
hasBiometricEncryptedUserKeyStored: boolean;
|
||||||
|
platformSupportsSecureStorage: boolean;
|
||||||
|
pinDecryptionAvailable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const table: [MockInputs, UnlockOptions][] = [
|
||||||
|
[
|
||||||
|
// MP + PIN + Biometrics available
|
||||||
|
{
|
||||||
|
hasMasterPassword: true,
|
||||||
|
osSupportsBiometric: true,
|
||||||
|
biometricLockSet: true,
|
||||||
|
hasBiometricEncryptedUserKeyStored: true,
|
||||||
|
biometricReady: true,
|
||||||
|
platformSupportsSecureStorage: true,
|
||||||
|
pinDecryptionAvailable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: true,
|
||||||
|
disableReason: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// PIN + Biometrics available
|
||||||
|
{
|
||||||
|
hasMasterPassword: false,
|
||||||
|
osSupportsBiometric: true,
|
||||||
|
biometricLockSet: true,
|
||||||
|
hasBiometricEncryptedUserKeyStored: true,
|
||||||
|
biometricReady: true,
|
||||||
|
platformSupportsSecureStorage: true,
|
||||||
|
pinDecryptionAvailable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: true,
|
||||||
|
disableReason: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// Biometrics available: user key stored with no secure storage
|
||||||
|
{
|
||||||
|
hasMasterPassword: false,
|
||||||
|
osSupportsBiometric: true,
|
||||||
|
biometricLockSet: true,
|
||||||
|
hasBiometricEncryptedUserKeyStored: true,
|
||||||
|
biometricReady: true,
|
||||||
|
platformSupportsSecureStorage: false,
|
||||||
|
pinDecryptionAvailable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: true,
|
||||||
|
disableReason: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// Biometrics available: no user key stored with no secure storage
|
||||||
|
{
|
||||||
|
hasMasterPassword: false,
|
||||||
|
osSupportsBiometric: true,
|
||||||
|
biometricLockSet: true,
|
||||||
|
hasBiometricEncryptedUserKeyStored: false,
|
||||||
|
biometricReady: true,
|
||||||
|
platformSupportsSecureStorage: false,
|
||||||
|
pinDecryptionAvailable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: true,
|
||||||
|
disableReason: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// Biometrics not available: biometric not ready
|
||||||
|
{
|
||||||
|
hasMasterPassword: false,
|
||||||
|
osSupportsBiometric: true,
|
||||||
|
biometricLockSet: true,
|
||||||
|
hasBiometricEncryptedUserKeyStored: true,
|
||||||
|
biometricReady: false,
|
||||||
|
platformSupportsSecureStorage: true,
|
||||||
|
pinDecryptionAvailable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: false,
|
||||||
|
disableReason: BiometricsDisableReason.SystemBiometricsUnavailable,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// Biometrics not available: biometric lock not set
|
||||||
|
{
|
||||||
|
hasMasterPassword: false,
|
||||||
|
osSupportsBiometric: true,
|
||||||
|
biometricLockSet: false,
|
||||||
|
hasBiometricEncryptedUserKeyStored: true,
|
||||||
|
biometricReady: true,
|
||||||
|
platformSupportsSecureStorage: true,
|
||||||
|
pinDecryptionAvailable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: false,
|
||||||
|
disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// Biometrics not available: user key not stored
|
||||||
|
{
|
||||||
|
hasMasterPassword: false,
|
||||||
|
osSupportsBiometric: true,
|
||||||
|
biometricLockSet: true,
|
||||||
|
hasBiometricEncryptedUserKeyStored: false,
|
||||||
|
biometricReady: true,
|
||||||
|
platformSupportsSecureStorage: true,
|
||||||
|
pinDecryptionAvailable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: false,
|
||||||
|
disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// Biometrics not available: OS doesn't support
|
||||||
|
{
|
||||||
|
hasMasterPassword: false,
|
||||||
|
osSupportsBiometric: false,
|
||||||
|
biometricLockSet: true,
|
||||||
|
hasBiometricEncryptedUserKeyStored: true,
|
||||||
|
biometricReady: true,
|
||||||
|
platformSupportsSecureStorage: true,
|
||||||
|
pinDecryptionAvailable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
masterPassword: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: false,
|
||||||
|
disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(table)("returns unlock options", async (mockInputs, expectedOutput) => {
|
||||||
|
const userId = "userId" as UserId;
|
||||||
|
const userDecryptionOptions = {
|
||||||
|
hasMasterPassword: mockInputs.hasMasterPassword,
|
||||||
|
};
|
||||||
|
|
||||||
|
// MP
|
||||||
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
|
of(userDecryptionOptions),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Biometrics
|
||||||
|
biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric);
|
||||||
|
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet);
|
||||||
|
cryptoService.hasUserKeyStored.mockResolvedValue(
|
||||||
|
mockInputs.hasBiometricEncryptedUserKeyStored,
|
||||||
|
);
|
||||||
|
platformUtilsService.supportsSecureStorage.mockReturnValue(
|
||||||
|
mockInputs.platformSupportsSecureStorage,
|
||||||
|
);
|
||||||
|
biometricEnabledMock.mockResolvedValue(mockInputs.biometricReady);
|
||||||
|
|
||||||
|
// PIN
|
||||||
|
pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable);
|
||||||
|
|
||||||
|
const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId));
|
||||||
|
|
||||||
|
expect(unlockOptions).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
129
apps/desktop/src/services/desktop-lock-component.service.ts
Normal file
129
apps/desktop/src/services/desktop-lock-component.service.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { inject } from "@angular/core";
|
||||||
|
import { combineLatest, defer, map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BiometricsDisableReason,
|
||||||
|
LockComponentService,
|
||||||
|
UnlockOptions,
|
||||||
|
} from "@bitwarden/auth/angular";
|
||||||
|
import {
|
||||||
|
PinServiceAbstraction,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
} from "@bitwarden/auth/common";
|
||||||
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { BiometricsService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
export class DesktopLockComponentService implements LockComponentService {
|
||||||
|
private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction);
|
||||||
|
private readonly platformUtilsService = inject(PlatformUtilsService);
|
||||||
|
private readonly biometricsService = inject(BiometricsService);
|
||||||
|
private readonly pinService = inject(PinServiceAbstraction);
|
||||||
|
private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||||
|
private readonly cryptoService = inject(CryptoService);
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
getBiometricsError(error: any): string | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviousUrl(): string | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isWindowVisible(): Promise<boolean> {
|
||||||
|
return ipc.platform.isWindowVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBiometricsUnlockBtnText(): string {
|
||||||
|
switch (this.platformUtilsService.getDevice()) {
|
||||||
|
case DeviceType.MacOsDesktop:
|
||||||
|
return "unlockWithTouchId";
|
||||||
|
case DeviceType.WindowsDesktop:
|
||||||
|
return "unlockWithWindowsHello";
|
||||||
|
case DeviceType.LinuxDesktop:
|
||||||
|
return "unlockWithPolkit";
|
||||||
|
default:
|
||||||
|
throw new Error("Unsupported platform");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isBiometricLockSet(userId: UserId): Promise<boolean> {
|
||||||
|
const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId);
|
||||||
|
const hasBiometricEncryptedUserKeyStored = await this.cryptoService.hasUserKeyStored(
|
||||||
|
KeySuffixOptions.Biometric,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isBiometricsSupportedAndReady(
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<{ supportsBiometric: boolean; biometricReady: boolean }> {
|
||||||
|
const supportsBiometric = await this.biometricsService.supportsBiometric();
|
||||||
|
const biometricReady = await ipc.keyManagement.biometric.enabled(userId);
|
||||||
|
return { supportsBiometric, biometricReady };
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions> {
|
||||||
|
return combineLatest([
|
||||||
|
// Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to
|
||||||
|
defer(() => this.isBiometricsSupportedAndReady(userId)),
|
||||||
|
defer(() => this.isBiometricLockSet(userId)),
|
||||||
|
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||||
|
defer(() => this.pinService.isPinDecryptionAvailable(userId)),
|
||||||
|
]).pipe(
|
||||||
|
map(
|
||||||
|
([biometricsData, isBiometricsLockSet, userDecryptionOptions, pinDecryptionAvailable]) => {
|
||||||
|
const disableReason = this.getBiometricsDisabledReason(
|
||||||
|
biometricsData.supportsBiometric,
|
||||||
|
isBiometricsLockSet,
|
||||||
|
biometricsData.biometricReady,
|
||||||
|
);
|
||||||
|
|
||||||
|
const unlockOpts: UnlockOptions = {
|
||||||
|
masterPassword: {
|
||||||
|
enabled: userDecryptionOptions.hasMasterPassword,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: pinDecryptionAvailable,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled:
|
||||||
|
biometricsData.supportsBiometric &&
|
||||||
|
isBiometricsLockSet &&
|
||||||
|
biometricsData.biometricReady,
|
||||||
|
disableReason: disableReason,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return unlockOpts;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBiometricsDisabledReason(
|
||||||
|
osSupportsBiometric: boolean,
|
||||||
|
biometricLockSet: boolean,
|
||||||
|
biometricReady: boolean,
|
||||||
|
): BiometricsDisableReason | null {
|
||||||
|
if (!osSupportsBiometric) {
|
||||||
|
return BiometricsDisableReason.NotSupportedOnOperatingSystem;
|
||||||
|
} else if (!biometricLockSet) {
|
||||||
|
return BiometricsDisableReason.EncryptedKeysUnavailable;
|
||||||
|
} else if (!biometricReady) {
|
||||||
|
return BiometricsDisableReason.SystemBiometricsUnavailable;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
|
import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common";
|
||||||
import { View } from "@bitwarden/common/src/models/view/view";
|
import { View } from "@bitwarden/common/src/models/view/view";
|
||||||
|
|
||||||
import { GroupDetailsResponse, GroupResponse } from "../services/group/responses/group.response";
|
import { GroupDetailsResponse, GroupResponse } from "../services/group/responses/group.response";
|
||||||
|
|
||||||
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
|
|
||||||
|
|
||||||
export class GroupView implements View {
|
export class GroupView implements View {
|
||||||
id: string;
|
id: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
export * from "./collection-access-selection.view";
|
|
||||||
export * from "./group.view";
|
export * from "./group.view";
|
||||||
export * from "./organization-user.view";
|
export * from "./organization-user.view";
|
||||||
export * from "./organization-user-admin-view";
|
export * from "./organization-user-admin-view";
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
|
import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common";
|
||||||
import {
|
import {
|
||||||
OrganizationUserStatusType,
|
OrganizationUserStatusType,
|
||||||
OrganizationUserType,
|
OrganizationUserType,
|
||||||
} from "@bitwarden/common/admin-console/enums";
|
} from "@bitwarden/common/admin-console/enums";
|
||||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||||
|
|
||||||
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
|
|
||||||
|
|
||||||
export class OrganizationUserAdminView {
|
export class OrganizationUserAdminView {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common";
|
import {
|
||||||
|
OrganizationUserUserDetailsResponse,
|
||||||
|
CollectionAccessSelectionView,
|
||||||
|
} from "@bitwarden/admin-console/common";
|
||||||
import {
|
import {
|
||||||
OrganizationUserStatusType,
|
OrganizationUserStatusType,
|
||||||
OrganizationUserType,
|
OrganizationUserType,
|
||||||
} from "@bitwarden/common/admin-console/enums";
|
} from "@bitwarden/common/admin-console/enums";
|
||||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||||
|
|
||||||
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
|
|
||||||
|
|
||||||
export class OrganizationUserView {
|
export class OrganizationUserView {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
@ -78,7 +78,7 @@ export class EntityEventsComponent implements OnInit {
|
|||||||
async load() {
|
async load() {
|
||||||
try {
|
try {
|
||||||
if (this.showUser) {
|
if (this.showUser) {
|
||||||
const response = await this.organizationUserApiService.getAllUsers(
|
const response = await this.organizationUserApiService.getAllMiniUserDetails(
|
||||||
this.params.organizationId,
|
this.params.organizationId,
|
||||||
);
|
);
|
||||||
response.data.forEach((u) => {
|
response.data.forEach((u) => {
|
||||||
|
@ -83,7 +83,9 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const response = await this.organizationUserApiService.getAllUsers(this.organizationId);
|
const response = await this.organizationUserApiService.getAllMiniUserDetails(
|
||||||
|
this.organizationId,
|
||||||
|
);
|
||||||
response.data.forEach((u) => {
|
response.data.forEach((u) => {
|
||||||
const name = this.userNamePipe.transform(u);
|
const name = this.userNamePipe.transform(u);
|
||||||
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });
|
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });
|
||||||
|
@ -14,7 +14,11 @@ import {
|
|||||||
takeUntil,
|
takeUntil,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
import {
|
||||||
|
CollectionAdminService,
|
||||||
|
CollectionAdminView,
|
||||||
|
OrganizationUserApiService,
|
||||||
|
} from "@bitwarden/admin-console/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
@ -26,8 +30,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { CollectionAdminService } from "../../../vault/core/collection-admin.service";
|
|
||||||
import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view";
|
|
||||||
import { InternalGroupService as GroupService, GroupView } from "../core";
|
import { InternalGroupService as GroupService, GroupView } from "../core";
|
||||||
import {
|
import {
|
||||||
AccessItemType,
|
AccessItemType,
|
||||||
@ -131,7 +133,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> {
|
private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> {
|
||||||
return from(this.organizationUserApiService.getAllUsers(this.organizationId)).pipe(
|
return from(this.organizationUserApiService.getAllMiniUserDetails(this.organizationId)).pipe(
|
||||||
map((response) =>
|
map((response) =>
|
||||||
response.data.map((m) => ({
|
response.data.map((m) => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
|
@ -8,6 +8,7 @@ import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-conso
|
|||||||
import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response";
|
import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response";
|
||||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
@ -31,6 +32,7 @@ export abstract class BaseBulkConfirmComponent implements OnInit {
|
|||||||
|
|
||||||
protected constructor(
|
protected constructor(
|
||||||
protected cryptoService: CryptoService,
|
protected cryptoService: CryptoService,
|
||||||
|
protected encryptService: EncryptService,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -67,7 +69,7 @@ export abstract class BaseBulkConfirmComponent implements OnInit {
|
|||||||
if (publicKey == null) {
|
if (publicKey == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const encryptedKey = await this.cryptoService.rsaEncrypt(key.key, publicKey);
|
const encryptedKey = await this.encryptService.rsaEncrypt(key.key, publicKey);
|
||||||
userIdsWithKeys.push({
|
userIdsWithKeys.push({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
key: encryptedKey.encryptedString,
|
key: encryptedKey.encryptedString,
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
@ -41,6 +42,7 @@ export class BulkConfirmComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) protected data: BulkConfirmDialogData,
|
@Inject(DIALOG_DATA) protected data: BulkConfirmDialogData,
|
||||||
protected cryptoService: CryptoService,
|
protected cryptoService: CryptoService,
|
||||||
|
protected encryptService: EncryptService,
|
||||||
protected apiService: ApiService,
|
protected apiService: ApiService,
|
||||||
private organizationUserApiService: OrganizationUserApiService,
|
private organizationUserApiService: OrganizationUserApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
@ -81,7 +83,7 @@ export class BulkConfirmComponent implements OnInit {
|
|||||||
if (publicKey == null) {
|
if (publicKey == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const encryptedKey = await this.cryptoService.rsaEncrypt(key.key, publicKey);
|
const encryptedKey = await this.encryptService.rsaEncrypt(key.key, publicKey);
|
||||||
userIdsWithKeys.push({
|
userIdsWithKeys.push({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
key: encryptedKey.encryptedString,
|
key: encryptedKey.encryptedString,
|
||||||
|
@ -13,7 +13,12 @@ import {
|
|||||||
takeUntil,
|
takeUntil,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
import {
|
||||||
|
CollectionAccessSelectionView,
|
||||||
|
CollectionAdminService,
|
||||||
|
CollectionAdminView,
|
||||||
|
OrganizationUserApiService,
|
||||||
|
} from "@bitwarden/admin-console/common";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import {
|
import {
|
||||||
OrganizationUserStatusType,
|
OrganizationUserStatusType,
|
||||||
@ -24,14 +29,10 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
|||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service";
|
|
||||||
import { CollectionAdminView } from "../../../../../vault/core/views/collection-admin.view";
|
|
||||||
import {
|
import {
|
||||||
CollectionAccessSelectionView,
|
|
||||||
GroupService,
|
GroupService,
|
||||||
GroupView,
|
GroupView,
|
||||||
OrganizationUserAdminView,
|
OrganizationUserAdminView,
|
||||||
@ -133,7 +134,6 @@ export class MemberDialogComponent implements OnDestroy {
|
|||||||
@Inject(DIALOG_DATA) protected params: MemberDialogParams,
|
@Inject(DIALOG_DATA) protected params: MemberDialogParams,
|
||||||
private dialogRef: DialogRef<MemberDialogResult>,
|
private dialogRef: DialogRef<MemberDialogResult>,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
// TODO: We should really look into consolidating naming conventions for these services
|
// TODO: We should really look into consolidating naming conventions for these services
|
||||||
private collectionAdminService: CollectionAdminService,
|
private collectionAdminService: CollectionAdminService,
|
||||||
|
@ -39,6 +39,7 @@ import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/
|
|||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
@ -107,6 +108,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
||||||
cryptoService: CryptoService,
|
cryptoService: CryptoService,
|
||||||
|
private encryptService: EncryptService,
|
||||||
validationService: ValidationService,
|
validationService: ValidationService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
userNamePipe: UserNamePipe,
|
userNamePipe: UserNamePipe,
|
||||||
@ -289,7 +291,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
|
|
||||||
async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise<void> {
|
async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise<void> {
|
||||||
const orgKey = await this.cryptoService.getOrgKey(this.organization.id);
|
const orgKey = await this.cryptoService.getOrgKey(this.organization.id);
|
||||||
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey);
|
const key = await this.encryptService.rsaEncrypt(orgKey.key, publicKey);
|
||||||
const request = new OrganizationUserConfirmRequest();
|
const request = new OrganizationUserConfirmRequest();
|
||||||
request.key = key.encryptedString;
|
request.key = key.encryptedString;
|
||||||
await this.organizationUserApiService.postOrganizationUserConfirm(
|
await this.organizationUserApiService.postOrganizationUserConfirm(
|
||||||
|
@ -71,7 +71,7 @@ describe("OrganizationUserResetPasswordService", () => {
|
|||||||
const mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
const mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
||||||
cryptoService.getUserKey.mockResolvedValue(mockUserKey);
|
cryptoService.getUserKey.mockResolvedValue(mockUserKey);
|
||||||
|
|
||||||
cryptoService.rsaEncrypt.mockResolvedValue(
|
encryptService.rsaEncrypt.mockResolvedValue(
|
||||||
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"),
|
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -103,7 +103,7 @@ describe("OrganizationUserResetPasswordService", () => {
|
|||||||
it("should rsa encrypt the user key", async () => {
|
it("should rsa encrypt the user key", async () => {
|
||||||
await sut.buildRecoveryKey(mockOrgId);
|
await sut.buildRecoveryKey(mockOrgId);
|
||||||
|
|
||||||
expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(expect.anything(), expect.anything());
|
expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(expect.anything(), expect.anything());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -128,7 +128,7 @@ describe("OrganizationUserResetPasswordService", () => {
|
|||||||
cryptoService.getOrgKey.mockResolvedValue(mockOrgKey);
|
cryptoService.getOrgKey.mockResolvedValue(mockOrgKey);
|
||||||
encryptService.decryptToBytes.mockResolvedValue(mockRandomBytes);
|
encryptService.decryptToBytes.mockResolvedValue(mockRandomBytes);
|
||||||
|
|
||||||
cryptoService.rsaDecrypt.mockResolvedValue(mockRandomBytes);
|
encryptService.rsaDecrypt.mockResolvedValue(mockRandomBytes);
|
||||||
const mockMasterKey = new SymmetricCryptoKey(mockRandomBytes) as MasterKey;
|
const mockMasterKey = new SymmetricCryptoKey(mockRandomBytes) as MasterKey;
|
||||||
cryptoService.makeMasterKey.mockResolvedValue(mockMasterKey);
|
cryptoService.makeMasterKey.mockResolvedValue(mockMasterKey);
|
||||||
cryptoService.hashMasterKey.mockResolvedValue("test-master-key-hash");
|
cryptoService.hashMasterKey.mockResolvedValue("test-master-key-hash");
|
||||||
@ -172,7 +172,7 @@ describe("OrganizationUserResetPasswordService", () => {
|
|||||||
publicKey: "test-public-key",
|
publicKey: "test-public-key",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
cryptoService.rsaEncrypt.mockResolvedValue(
|
encryptService.rsaEncrypt.mockResolvedValue(
|
||||||
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"),
|
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -57,7 +57,7 @@ export class OrganizationUserResetPasswordService
|
|||||||
if (userKey == null) {
|
if (userKey == null) {
|
||||||
throw new Error("No user key found");
|
throw new Error("No user key found");
|
||||||
}
|
}
|
||||||
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
|
const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey);
|
||||||
|
|
||||||
return encryptedKey.encryptedString;
|
return encryptedKey.encryptedString;
|
||||||
}
|
}
|
||||||
@ -96,7 +96,10 @@ export class OrganizationUserResetPasswordService
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Decrypt User's Reset Password Key to get UserKey
|
// Decrypt User's Reset Password Key to get UserKey
|
||||||
const decValue = await this.cryptoService.rsaDecrypt(response.resetPasswordKey, decPrivateKey);
|
const decValue = await this.encryptService.rsaDecrypt(
|
||||||
|
new EncString(response.resetPasswordKey),
|
||||||
|
decPrivateKey,
|
||||||
|
);
|
||||||
const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey;
|
const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey;
|
||||||
|
|
||||||
// determine Kdf Algorithm
|
// determine Kdf Algorithm
|
||||||
|
@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { CollectionAdminService } from "@bitwarden/admin-console/common";
|
||||||
import {
|
import {
|
||||||
canAccessVaultTab,
|
canAccessVaultTab,
|
||||||
OrganizationService,
|
OrganizationService,
|
||||||
@ -11,7 +12,6 @@ import { ImportComponent } from "@bitwarden/importer/ui";
|
|||||||
|
|
||||||
import { LooseComponentsModule, SharedModule } from "../../../shared";
|
import { LooseComponentsModule, SharedModule } from "../../../shared";
|
||||||
import { ImportCollectionAdminService } from "../../../tools/import/import-collection-admin.service";
|
import { ImportCollectionAdminService } from "../../../tools/import/import-collection-admin.service";
|
||||||
import { CollectionAdminService } from "../../../vault/core/collection-admin.service";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: "org-import.component.html",
|
templateUrl: "org-import.component.html",
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common";
|
import {
|
||||||
|
CollectionAccessSelectionView,
|
||||||
|
OrganizationUserUserDetailsResponse,
|
||||||
|
} from "@bitwarden/admin-console/common";
|
||||||
import {
|
import {
|
||||||
OrganizationUserStatusType,
|
OrganizationUserStatusType,
|
||||||
OrganizationUserType,
|
OrganizationUserType,
|
||||||
} from "@bitwarden/common/admin-console/enums";
|
} from "@bitwarden/common/admin-console/enums";
|
||||||
import { SelectItemView } from "@bitwarden/components";
|
import { SelectItemView } from "@bitwarden/components";
|
||||||
|
|
||||||
import { CollectionAccessSelectionView, GroupView } from "../../../core";
|
import { GroupView } from "../../../core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permission options that replace/correspond with manage, readOnly, and hidePassword server fields.
|
* Permission options that replace/correspond with manage, readOnly, and hidePassword server fields.
|
||||||
|
@ -2,3 +2,4 @@ export * from "./login";
|
|||||||
export * from "./webauthn-login";
|
export * from "./webauthn-login";
|
||||||
export * from "./set-password-jit";
|
export * from "./set-password-jit";
|
||||||
export * from "./registration";
|
export * from "./registration";
|
||||||
|
export * from "./web-lock-component.service";
|
||||||
|
@ -35,7 +35,7 @@ describe("RotateableKeySetService", () => {
|
|||||||
const encryptedPrivateKey = Symbol();
|
const encryptedPrivateKey = Symbol();
|
||||||
cryptoService.makeKeyPair.mockResolvedValue(["publicKey", encryptedPrivateKey as any]);
|
cryptoService.makeKeyPair.mockResolvedValue(["publicKey", encryptedPrivateKey as any]);
|
||||||
cryptoService.getUserKey.mockResolvedValue({ key: userKey.key } as any);
|
cryptoService.getUserKey.mockResolvedValue({ key: userKey.key } as any);
|
||||||
cryptoService.rsaEncrypt.mockResolvedValue(encryptedUserKey as any);
|
encryptService.rsaEncrypt.mockResolvedValue(encryptedUserKey as any);
|
||||||
encryptService.encrypt.mockResolvedValue(encryptedPublicKey as any);
|
encryptService.encrypt.mockResolvedValue(encryptedPublicKey as any);
|
||||||
|
|
||||||
const result = await service.createKeySet(externalKey as any);
|
const result = await service.createKeySet(externalKey as any);
|
||||||
|
@ -25,7 +25,7 @@ export class RotateableKeySetService {
|
|||||||
|
|
||||||
const userKey = await this.cryptoService.getUserKey();
|
const userKey = await this.cryptoService.getUserKey();
|
||||||
const rawPublicKey = Utils.fromB64ToArray(publicKey);
|
const rawPublicKey = Utils.fromB64ToArray(publicKey);
|
||||||
const encryptedUserKey = await this.cryptoService.rsaEncrypt(userKey.key, rawPublicKey);
|
const encryptedUserKey = await this.encryptService.rsaEncrypt(userKey.key, rawPublicKey);
|
||||||
const encryptedPublicKey = await this.encryptService.encrypt(rawPublicKey, userKey);
|
const encryptedPublicKey = await this.encryptService.encrypt(rawPublicKey, userKey);
|
||||||
return new RotateableKeySet(encryptedUserKey, encryptedPublicKey, encryptedPrivateKey);
|
return new RotateableKeySet(encryptedUserKey, encryptedPublicKey, encryptedPrivateKey);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,94 @@
|
|||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { WebLockComponentService } from "./web-lock-component.service";
|
||||||
|
|
||||||
|
describe("WebLockComponentService", () => {
|
||||||
|
let service: WebLockComponentService;
|
||||||
|
|
||||||
|
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
WebLockComponentService,
|
||||||
|
{
|
||||||
|
provide: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
useValue: userDecryptionOptionsService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(WebLockComponentService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("instantiates", () => {
|
||||||
|
expect(service).not.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getBiometricsError", () => {
|
||||||
|
it("throws an error when given a null input", () => {
|
||||||
|
expect(() => service.getBiometricsError(null)).toThrow(
|
||||||
|
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("throws an error when given a non-null input", () => {
|
||||||
|
expect(() => service.getBiometricsError("error")).toThrow(
|
||||||
|
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPreviousUrl", () => {
|
||||||
|
it("returns null", () => {
|
||||||
|
expect(service.getPreviousUrl()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isWindowVisible", () => {
|
||||||
|
it("throws an error", async () => {
|
||||||
|
await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getBiometricsUnlockBtnText", () => {
|
||||||
|
it("throws an error", () => {
|
||||||
|
expect(() => service.getBiometricsUnlockBtnText()).toThrow(
|
||||||
|
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAvailableUnlockOptions$", () => {
|
||||||
|
it("returns an observable of unlock options", async () => {
|
||||||
|
const userId = "user-id" as UserId;
|
||||||
|
const userDecryptionOptions = {
|
||||||
|
hasMasterPassword: true,
|
||||||
|
};
|
||||||
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValueOnce(
|
||||||
|
of(userDecryptionOptions),
|
||||||
|
);
|
||||||
|
|
||||||
|
const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId));
|
||||||
|
|
||||||
|
expect(unlockOptions).toEqual({
|
||||||
|
masterPassword: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: false,
|
||||||
|
disableReason: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,55 @@
|
|||||||
|
import { inject } from "@angular/core";
|
||||||
|
import { map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { LockComponentService, UnlockOptions } from "@bitwarden/auth/angular";
|
||||||
|
import {
|
||||||
|
UserDecryptionOptions,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
} from "@bitwarden/auth/common";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
export class WebLockComponentService implements LockComponentService {
|
||||||
|
private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction);
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
getBiometricsError(error: any): string | null {
|
||||||
|
throw new Error(
|
||||||
|
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviousUrl(): string | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isWindowVisible(): Promise<boolean> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
getBiometricsUnlockBtnText(): string {
|
||||||
|
throw new Error(
|
||||||
|
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions> {
|
||||||
|
return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe(
|
||||||
|
map((userDecryptionOptions: UserDecryptionOptions) => {
|
||||||
|
const unlockOpts: UnlockOptions = {
|
||||||
|
masterPassword: {
|
||||||
|
enabled: userDecryptionOptions.hasMasterPassword,
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
enabled: false,
|
||||||
|
disableReason: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return unlockOpts;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -132,7 +132,7 @@ describe("EmergencyAccessService", () => {
|
|||||||
cryptoService.getUserKey.mockResolvedValueOnce(mockUserKey);
|
cryptoService.getUserKey.mockResolvedValueOnce(mockUserKey);
|
||||||
apiService.getUserPublicKey.mockResolvedValueOnce(mockUserPublicKeyResponse);
|
apiService.getUserPublicKey.mockResolvedValueOnce(mockUserPublicKeyResponse);
|
||||||
|
|
||||||
cryptoService.rsaEncrypt.mockResolvedValueOnce(mockUserPublicKeyEncryptedUserKey);
|
encryptService.rsaEncrypt.mockResolvedValueOnce(mockUserPublicKeyEncryptedUserKey);
|
||||||
|
|
||||||
emergencyAccessApiService.postEmergencyAccessConfirm.mockResolvedValueOnce();
|
emergencyAccessApiService.postEmergencyAccessConfirm.mockResolvedValueOnce();
|
||||||
|
|
||||||
@ -162,7 +162,7 @@ describe("EmergencyAccessService", () => {
|
|||||||
|
|
||||||
const mockDecryptedGrantorUserKey = new Uint8Array(64);
|
const mockDecryptedGrantorUserKey = new Uint8Array(64);
|
||||||
cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64));
|
cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64));
|
||||||
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey);
|
encryptService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey);
|
||||||
|
|
||||||
const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
|
const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
|
||||||
|
|
||||||
@ -200,7 +200,7 @@ describe("EmergencyAccessService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not post a new password if decryption fails", async () => {
|
it("should not post a new password if decryption fails", async () => {
|
||||||
cryptoService.rsaDecrypt.mockResolvedValueOnce(null);
|
encryptService.rsaDecrypt.mockResolvedValueOnce(null);
|
||||||
emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce({
|
emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce({
|
||||||
keyEncrypted: "EncryptedKey",
|
keyEncrypted: "EncryptedKey",
|
||||||
kdf: KdfType.PBKDF2_SHA256,
|
kdf: KdfType.PBKDF2_SHA256,
|
||||||
@ -259,7 +259,7 @@ describe("EmergencyAccessService", () => {
|
|||||||
publicKey: "mockPublicKey",
|
publicKey: "mockPublicKey",
|
||||||
} as UserKeyResponse);
|
} as UserKeyResponse);
|
||||||
|
|
||||||
cryptoService.rsaEncrypt.mockImplementation((plainValue, publicKey) => {
|
encryptService.rsaEncrypt.mockImplementation((plainValue, publicKey) => {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "Encrypted: " + plainValue),
|
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "Encrypted: " + plainValue),
|
||||||
);
|
);
|
||||||
|
@ -17,7 +17,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { KdfType } from "@bitwarden/common/platform/enums";
|
import { KdfType } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
import { UserKey } from "@bitwarden/common/types/key";
|
||||||
@ -224,8 +224,8 @@ export class EmergencyAccessService
|
|||||||
throw new Error("Active user does not have a private key, cannot get view only ciphers.");
|
throw new Error("Active user does not have a private key, cannot get view only ciphers.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(
|
const grantorKeyBuffer = await this.encryptService.rsaDecrypt(
|
||||||
response.keyEncrypted,
|
new EncString(response.keyEncrypted),
|
||||||
activeUserPrivateKey,
|
activeUserPrivateKey,
|
||||||
);
|
);
|
||||||
const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey;
|
const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey;
|
||||||
@ -261,8 +261,8 @@ export class EmergencyAccessService
|
|||||||
throw new Error("Active user does not have a private key, cannot complete a takeover.");
|
throw new Error("Active user does not have a private key, cannot complete a takeover.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(
|
const grantorKeyBuffer = await this.encryptService.rsaDecrypt(
|
||||||
takeoverResponse.keyEncrypted,
|
new EncString(takeoverResponse.keyEncrypted),
|
||||||
activeUserPrivateKey,
|
activeUserPrivateKey,
|
||||||
);
|
);
|
||||||
if (grantorKeyBuffer == null) {
|
if (grantorKeyBuffer == null) {
|
||||||
@ -355,6 +355,6 @@ export class EmergencyAccessService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async encryptKey(userKey: UserKey, publicKey: Uint8Array): Promise<EncryptedString> {
|
private async encryptKey(userKey: UserKey, publicKey: Uint8Array): Promise<EncryptedString> {
|
||||||
return (await this.cryptoService.rsaEncrypt(userKey.key, publicKey)).encryptedString;
|
return (await this.encryptService.rsaEncrypt(userKey.key, publicKey)).encryptedString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,7 @@ export class AcceptOrganizationInviteService {
|
|||||||
|
|
||||||
// RSA Encrypt user's encKey.key with organization public key
|
// RSA Encrypt user's encKey.key with organization public key
|
||||||
const userKey = await this.cryptoService.getUserKey();
|
const userKey = await this.cryptoService.getUserKey();
|
||||||
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
|
const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey);
|
||||||
|
|
||||||
// Add reset password key to accept request
|
// Add reset password key to accept request
|
||||||
request.resetPasswordKey = encryptedKey.encryptedString;
|
request.resetPasswordKey = encryptedKey.encryptedString;
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
<dl>
|
<dl>
|
||||||
<dt>{{ "status" | i18n }}</dt>
|
<dt>{{ "status" | i18n }}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<span class="tw-capitalize">{{ (subscription && subscription.status) || "-" }}</span>
|
<span class="tw-capitalize">{{ (subscription && subscriptionStatus) || "-" }}</span>
|
||||||
<span bitBadge variant="warning" *ngIf="subscriptionMarkedForCancel">{{
|
<span bitBadge variant="warning" *ngIf="subscriptionMarkedForCancel">{{
|
||||||
"pendingCancellation" | i18n
|
"pendingCancellation" | i18n
|
||||||
}}</span>
|
}}</span>
|
||||||
|
@ -35,8 +35,6 @@ import { UpdateLicenseDialogResult } from "../shared/update-license-types";
|
|||||||
export class UserSubscriptionComponent implements OnInit {
|
export class UserSubscriptionComponent implements OnInit {
|
||||||
loading = false;
|
loading = false;
|
||||||
firstLoaded = false;
|
firstLoaded = false;
|
||||||
adjustStorageAdd = true;
|
|
||||||
showUpdateLicense = false;
|
|
||||||
sub: SubscriptionResponse;
|
sub: SubscriptionResponse;
|
||||||
selfHosted = false;
|
selfHosted = false;
|
||||||
cloudWebVaultUrl: string;
|
cloudWebVaultUrl: string;
|
||||||
@ -65,7 +63,7 @@ export class UserSubscriptionComponent implements OnInit {
|
|||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.selfHosted = platformUtilsService.isSelfHost();
|
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -216,11 +214,28 @@ export class UserSubscriptionComponent implements OnInit {
|
|||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get storageProgressWidth() {
|
|
||||||
return this.storagePercentage < 5 ? 5 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get title(): string {
|
get title(): string {
|
||||||
return this.i18nService.t(this.selfHosted ? "subscription" : "premiumMembership");
|
return this.i18nService.t(this.selfHosted ? "subscription" : "premiumMembership");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get subscriptionStatus(): string | null {
|
||||||
|
if (!this.subscription) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
Premium users who sign up with PayPal will have their subscription activated by a webhook.
|
||||||
|
This is an arbitrary 15-second grace period where we show their subscription as active rather than
|
||||||
|
incomplete while we wait for our webhook to process the `invoice.created` event.
|
||||||
|
*/
|
||||||
|
if (this.subscription.status === "incomplete") {
|
||||||
|
const periodStartMS = new Date(this.subscription.periodStartDate).getTime();
|
||||||
|
const nowMS = new Date().getTime();
|
||||||
|
return nowMS - periodStartMS <= 15000
|
||||||
|
? this.i18nService.t("active")
|
||||||
|
: this.subscription.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.subscription.status;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
<bit-dialog>
|
<bit-dialog>
|
||||||
<h1 bitDialogTitle>
|
<h1 bitDialogTitle>
|
||||||
{{ (hasBillingToken ? "viewBillingSyncToken" : "generateBillingSyncToken") | i18n }}
|
{{ (hasBillingToken ? "viewBillingToken" : "generateBillingToken") | i18n }}
|
||||||
</h1>
|
</h1>
|
||||||
<div bitDialogContent>
|
<div bitDialogContent>
|
||||||
<app-user-verification formControlName="verification" *ngIf="!clientSecret">
|
<app-user-verification formControlName="verification" *ngIf="!clientSecret">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
<bit-dialog>
|
<bit-dialog>
|
||||||
<h1 bitDialogTitle>
|
<h1 bitDialogTitle>
|
||||||
{{ "manageBillingSync" | i18n }}
|
{{ "manageBillingTokenSync" | i18n }}
|
||||||
</h1>
|
</h1>
|
||||||
<div bitDialogContent>
|
<div bitDialogContent>
|
||||||
<p>{{ "billingSyncKeyDesc" | i18n }}</p>
|
<p>{{ "billingSyncKeyDesc" | i18n }}</p>
|
||||||
|
@ -280,7 +280,7 @@
|
|||||||
(click)="manageBillingSync()"
|
(click)="manageBillingSync()"
|
||||||
*ngIf="canManageBillingSync"
|
*ngIf="canManageBillingSync"
|
||||||
>
|
>
|
||||||
{{ (hasBillingSyncToken ? "manageBillingSync" : "setUpBillingSync") | i18n }}
|
{{ (hasBillingSyncToken ? "viewBillingToken" : "setUpBillingSync") | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="userOrg.canEditSubscription">
|
<ng-container *ngIf="userOrg.canEditSubscription">
|
||||||
|
@ -90,7 +90,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</bit-label>
|
</bit-label>
|
||||||
<bit-hint>
|
<bit-hint>
|
||||||
{{ "billingSyncDesc" | i18n }}
|
{{ "automaticBillingSyncDesc" | i18n }}
|
||||||
</bit-hint>
|
</bit-hint>
|
||||||
</bit-radio-button>
|
</bit-radio-button>
|
||||||
<ng-container *ngIf="updateMethod === licenseOptions.SYNC">
|
<ng-container *ngIf="updateMethod === licenseOptions.SYNC">
|
||||||
@ -100,7 +100,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="manageBillingSyncSelfHosted()"
|
(click)="manageBillingSyncSelfHosted()"
|
||||||
>
|
>
|
||||||
{{ "manageBillingSync" | i18n }}
|
{{ "manageBillingTokenSync" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
bitButton
|
bitButton
|
||||||
@ -121,7 +121,7 @@
|
|||||||
>
|
>
|
||||||
<bit-label>{{ "manualUpload" | i18n }}</bit-label>
|
<bit-label>{{ "manualUpload" | i18n }}</bit-label>
|
||||||
<bit-hint>
|
<bit-hint>
|
||||||
{{ "manualUploadDesc" | i18n }}
|
{{ "manualBillingTokenUploadDesc" | i18n }}
|
||||||
</bit-hint>
|
</bit-hint>
|
||||||
</bit-radio-button>
|
</bit-radio-button>
|
||||||
<ng-container *ngIf="updateMethod === licenseOptions.UPLOAD">
|
<ng-container *ngIf="updateMethod === licenseOptions.UPLOAD">
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core";
|
import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core";
|
||||||
|
|
||||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
import {
|
||||||
|
CollectionAdminService,
|
||||||
|
DefaultCollectionAdminService,
|
||||||
|
OrganizationUserApiService,
|
||||||
|
} from "@bitwarden/admin-console/common";
|
||||||
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||||
import {
|
import {
|
||||||
CLIENT_TYPE,
|
CLIENT_TYPE,
|
||||||
@ -21,6 +25,7 @@ import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/serv
|
|||||||
import {
|
import {
|
||||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||||
LoginComponentService,
|
LoginComponentService,
|
||||||
|
LockComponentService,
|
||||||
SetPasswordJitService,
|
SetPasswordJitService,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import {
|
import {
|
||||||
@ -42,6 +47,7 @@ import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.ser
|
|||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@ -66,6 +72,7 @@ import {
|
|||||||
} from "@bitwarden/common/platform/theming/theme-state.service";
|
} from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||||
|
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||||
import { BiometricsService } from "@bitwarden/key-management";
|
import { BiometricsService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||||
@ -73,6 +80,7 @@ import {
|
|||||||
WebSetPasswordJitService,
|
WebSetPasswordJitService,
|
||||||
WebRegistrationFinishService,
|
WebRegistrationFinishService,
|
||||||
WebLoginComponentService,
|
WebLoginComponentService,
|
||||||
|
WebLockComponentService,
|
||||||
} from "../auth";
|
} from "../auth";
|
||||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||||
import { HtmlStorageService } from "../core/html-storage.service";
|
import { HtmlStorageService } from "../core/html-storage.service";
|
||||||
@ -81,7 +89,6 @@ import { WebBiometricsService } from "../key-management/web-biometric.service";
|
|||||||
import { WebEnvironmentService } from "../platform/web-environment.service";
|
import { WebEnvironmentService } from "../platform/web-environment.service";
|
||||||
import { WebMigrationRunner } from "../platform/web-migration-runner";
|
import { WebMigrationRunner } from "../platform/web-migration-runner";
|
||||||
import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
|
import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
|
||||||
import { CollectionAdminService } from "../vault/core/collection-admin.service";
|
|
||||||
|
|
||||||
import { EventService } from "./event.service";
|
import { EventService } from "./event.service";
|
||||||
import { InitService } from "./init.service";
|
import { InitService } from "./init.service";
|
||||||
@ -155,7 +162,6 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: WebFileDownloadService,
|
useClass: WebFileDownloadService,
|
||||||
useAngularDecorators: true,
|
useAngularDecorators: true,
|
||||||
}),
|
}),
|
||||||
safeProvider(CollectionAdminService),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: WindowStorageService,
|
provide: WindowStorageService,
|
||||||
useFactory: () => new WindowStorageService(window.localStorage),
|
useFactory: () => new WindowStorageService(window.localStorage),
|
||||||
@ -208,12 +214,18 @@ const safeProviders: SafeProvider[] = [
|
|||||||
PolicyService,
|
PolicyService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: LockComponentService,
|
||||||
|
useClass: WebLockComponentService,
|
||||||
|
deps: [],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: SetPasswordJitService,
|
provide: SetPasswordJitService,
|
||||||
useClass: WebSetPasswordJitService,
|
useClass: WebSetPasswordJitService,
|
||||||
deps: [
|
deps: [
|
||||||
ApiService,
|
ApiService,
|
||||||
CryptoServiceAbstraction,
|
CryptoServiceAbstraction,
|
||||||
|
EncryptService,
|
||||||
I18nServiceAbstraction,
|
I18nServiceAbstraction,
|
||||||
KdfConfigService,
|
KdfConfigService,
|
||||||
InternalMasterPasswordServiceAbstraction,
|
InternalMasterPasswordServiceAbstraction,
|
||||||
@ -243,6 +255,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: LoginEmailService,
|
useClass: LoginEmailService,
|
||||||
deps: [AccountService, AuthService, StateProvider],
|
deps: [AccountService, AuthService, StateProvider],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: CollectionAdminService,
|
||||||
|
useClass: DefaultCollectionAdminService,
|
||||||
|
deps: [ApiService, CryptoServiceAbstraction, EncryptService, CollectionService],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
unauthGuardFn,
|
unauthGuardFn,
|
||||||
} from "@bitwarden/angular/auth/guards";
|
} from "@bitwarden/angular/auth/guards";
|
||||||
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||||
|
import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap";
|
||||||
import {
|
import {
|
||||||
AnonLayoutWrapperComponent,
|
AnonLayoutWrapperComponent,
|
||||||
AnonLayoutWrapperData,
|
AnonLayoutWrapperData,
|
||||||
@ -22,6 +23,7 @@ import {
|
|||||||
RegistrationLinkExpiredComponent,
|
RegistrationLinkExpiredComponent,
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
LoginSecondaryContentComponent,
|
LoginSecondaryContentComponent,
|
||||||
|
LockV2Component,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
UserLockIcon,
|
UserLockIcon,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
@ -367,20 +369,58 @@ const routes: Routes = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "lock",
|
path: "login",
|
||||||
canActivate: [deepLinkGuard(), lockGuard()],
|
canActivate: [unauthGuardFn()],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
component: LockComponent,
|
component: LoginComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: EnvironmentSelectorComponent,
|
||||||
|
outlet: "environment-selector",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
data: {
|
data: {
|
||||||
pageTitle: "yourVaultIsLockedV2",
|
pageTitle: "logIn",
|
||||||
pageIcon: LockIcon,
|
},
|
||||||
showReadonlyHostname: true,
|
|
||||||
} satisfies AnonLayoutWrapperData,
|
|
||||||
},
|
},
|
||||||
|
...extensionRefreshSwap(
|
||||||
|
LockComponent,
|
||||||
|
LockV2Component,
|
||||||
|
{
|
||||||
|
path: "lock",
|
||||||
|
canActivate: [deepLinkGuard(), lockGuard()],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: LockComponent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: {
|
||||||
|
pageTitle: "yourVaultIsLockedV2",
|
||||||
|
pageIcon: LockIcon,
|
||||||
|
showReadonlyHostname: true,
|
||||||
|
} satisfies AnonLayoutWrapperData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "lock",
|
||||||
|
canActivate: [deepLinkGuard(), lockGuard()],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: LockV2Component,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: {
|
||||||
|
pageTitle: "yourAccountIsLocked",
|
||||||
|
pageIcon: LockIcon,
|
||||||
|
showReadonlyHostname: true,
|
||||||
|
} satisfies AnonLayoutWrapperData,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "2fa",
|
path: "2fa",
|
||||||
canActivate: [unauthGuardFn()],
|
canActivate: [unauthGuardFn()],
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
|
import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||||
|
|
||||||
import { ImportCollectionServiceAbstraction } from "../../../../../../libs/importer/src/services/import-collection.service.abstraction";
|
import { ImportCollectionServiceAbstraction } from "../../../../../../libs/importer/src/services/import-collection.service.abstraction";
|
||||||
import { CollectionAdminService } from "../../vault/core/collection-admin.service";
|
|
||||||
import { CollectionAdminView } from "../../vault/core/views/collection-admin.view";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImportCollectionAdminService implements ImportCollectionServiceAbstraction {
|
export class ImportCollectionAdminService implements ImportCollectionServiceAbstraction {
|
||||||
|
@ -14,8 +14,11 @@ import {
|
|||||||
import { first } from "rxjs/operators";
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CollectionAccessSelectionView,
|
||||||
|
CollectionAdminService,
|
||||||
|
CollectionAdminView,
|
||||||
OrganizationUserApiService,
|
OrganizationUserApiService,
|
||||||
OrganizationUserUserDetailsResponse,
|
OrganizationUserUserMiniResponse,
|
||||||
} from "@bitwarden/admin-console/common";
|
} from "@bitwarden/admin-console/common";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
@ -26,11 +29,7 @@ import { CollectionResponse } from "@bitwarden/common/vault/models/response/coll
|
|||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
import { BitValidators, DialogService } from "@bitwarden/components";
|
import { BitValidators, DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import {
|
import { GroupService, GroupView } from "../../../admin-console/organizations/core";
|
||||||
CollectionAccessSelectionView,
|
|
||||||
GroupService,
|
|
||||||
GroupView,
|
|
||||||
} from "../../../admin-console/organizations/core";
|
|
||||||
import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component";
|
import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component";
|
||||||
import {
|
import {
|
||||||
AccessItemType,
|
AccessItemType,
|
||||||
@ -40,8 +39,6 @@ import {
|
|||||||
convertToPermission,
|
convertToPermission,
|
||||||
convertToSelectionView,
|
convertToSelectionView,
|
||||||
} from "../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
|
} from "../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
|
||||||
import { CollectionAdminService } from "../../core/collection-admin.service";
|
|
||||||
import { CollectionAdminView } from "../../core/views/collection-admin.view";
|
|
||||||
|
|
||||||
export enum CollectionDialogTabType {
|
export enum CollectionDialogTabType {
|
||||||
Info = 0,
|
Info = 0,
|
||||||
@ -156,15 +153,23 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
organization: organization$,
|
organization: organization$,
|
||||||
collections: this.collectionAdminService.getAll(orgId),
|
collections: this.collectionAdminService.getAll(orgId),
|
||||||
groups: groups$,
|
groups: groups$,
|
||||||
// Collection(s) needed to map readonlypermission for (potential) access selector disabled state
|
users: this.organizationUserApiService.getAllMiniUserDetails(orgId),
|
||||||
users: this.organizationUserApiService.getAllUsers(orgId, { includeCollections: true }),
|
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$))
|
.pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$))
|
||||||
.subscribe(({ organization, collections: allCollections, groups, users }) => {
|
.subscribe(({ organization, collections: allCollections, groups, users }) => {
|
||||||
this.organization = organization;
|
this.organization = organization;
|
||||||
|
|
||||||
|
if (this.params.collectionId) {
|
||||||
|
this.collection = allCollections.find((c) => c.id === this.collectionId);
|
||||||
|
|
||||||
|
if (!this.collection) {
|
||||||
|
throw new Error("Could not find collection to edit.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.accessItems = [].concat(
|
this.accessItems = [].concat(
|
||||||
groups.map((group) => mapGroupToAccessItemView(group, this.collectionId)),
|
groups.map((group) => mapGroupToAccessItemView(group, this.collection)),
|
||||||
users.data.map((user) => mapUserToAccessItemView(user, this.collectionId)),
|
users.data.map((user) => mapUserToAccessItemView(user, this.collection)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Force change detection to update the access selector's items
|
// Force change detection to update the access selector's items
|
||||||
@ -174,15 +179,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
? allCollections.filter((c) => c.manage)
|
? allCollections.filter((c) => c.manage)
|
||||||
: allCollections;
|
: allCollections;
|
||||||
|
|
||||||
if (this.params.collectionId) {
|
if (this.collection) {
|
||||||
this.collection = allCollections.find((c) => c.id === this.collectionId);
|
|
||||||
// Ensure we don't allow nesting the current collection within itself
|
// Ensure we don't allow nesting the current collection within itself
|
||||||
this.nestOptions = this.nestOptions.filter((c) => c.id !== this.collectionId);
|
this.nestOptions = this.nestOptions.filter((c) => c.id !== this.collectionId);
|
||||||
|
|
||||||
if (!this.collection) {
|
|
||||||
throw new Error("Could not find collection to edit.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the name to find its parent name
|
// Parse the name to find its parent name
|
||||||
const { name, parent: parentName } = parseName(this.collection);
|
const { name, parent: parentName } = parseName(this.collection);
|
||||||
|
|
||||||
@ -423,7 +423,10 @@ function validateCanManagePermission(control: AbstractControl) {
|
|||||||
* @param collectionId Current collection being viewed/edited
|
* @param collectionId Current collection being viewed/edited
|
||||||
* @returns AccessItemView customized to set a readonlyPermission to be displayed if the access selector is in a disabled state
|
* @returns AccessItemView customized to set a readonlyPermission to be displayed if the access selector is in a disabled state
|
||||||
*/
|
*/
|
||||||
function mapGroupToAccessItemView(group: GroupView, collectionId: string): AccessItemView {
|
function mapGroupToAccessItemView(
|
||||||
|
group: GroupView,
|
||||||
|
collection: CollectionAdminView,
|
||||||
|
): AccessItemView {
|
||||||
return {
|
return {
|
||||||
id: group.id,
|
id: group.id,
|
||||||
type: AccessItemType.Group,
|
type: AccessItemType.Group,
|
||||||
@ -431,8 +434,8 @@ function mapGroupToAccessItemView(group: GroupView, collectionId: string): Acces
|
|||||||
labelName: group.name,
|
labelName: group.name,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
readonlyPermission:
|
readonlyPermission:
|
||||||
collectionId != null
|
collection != null
|
||||||
? convertToPermission(group.collections.find((gc) => gc.id == collectionId))
|
? convertToPermission(collection.groups.find((g) => g.id === group.id))
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -444,8 +447,8 @@ function mapGroupToAccessItemView(group: GroupView, collectionId: string): Acces
|
|||||||
* @returns AccessItemView customized to set a readonlyPermission to be displayed if the access selector is in a disabled state
|
* @returns AccessItemView customized to set a readonlyPermission to be displayed if the access selector is in a disabled state
|
||||||
*/
|
*/
|
||||||
function mapUserToAccessItemView(
|
function mapUserToAccessItemView(
|
||||||
user: OrganizationUserUserDetailsResponse,
|
user: OrganizationUserUserMiniResponse,
|
||||||
collectionId: string,
|
collection: CollectionAdminView,
|
||||||
): AccessItemView {
|
): AccessItemView {
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@ -457,9 +460,9 @@ function mapUserToAccessItemView(
|
|||||||
status: user.status,
|
status: user.status,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
readonlyPermission:
|
readonlyPermission:
|
||||||
collectionId != null
|
collection != null
|
||||||
? convertToPermission(
|
? convertToPermission(
|
||||||
new CollectionAccessSelectionView(user.collections.find((uc) => uc.id == collectionId)),
|
new CollectionAccessSelectionView(collection.users.find((u) => u.id === user.id)),
|
||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { CollectionAdminView, Unassigned } from "@bitwarden/admin-console/common";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
|
|
||||||
import { GroupView } from "../../../admin-console/organizations/core";
|
import { GroupView } from "../../../admin-console/organizations/core";
|
||||||
import { CollectionAdminView } from "../../core/views/collection-admin.view";
|
|
||||||
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
convertToPermission,
|
convertToPermission,
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { SelectionModel } from "@angular/cdk/collections";
|
import { SelectionModel } from "@angular/cdk/collections";
|
||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
import { TableDataSource } from "@bitwarden/components";
|
import { TableDataSource } from "@bitwarden/components";
|
||||||
|
|
||||||
import { GroupView } from "../../../admin-console/organizations/core";
|
import { GroupView } from "../../../admin-console/organizations/core";
|
||||||
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
|
||||||
|
|
||||||
import { VaultItem } from "./vault-item";
|
import { VaultItem } from "./vault-item";
|
||||||
import { VaultItemEvent } from "./vault-item-event";
|
import { VaultItemEvent } from "./vault-item-event";
|
||||||
|
@ -3,6 +3,11 @@ import { RouterModule } from "@angular/router";
|
|||||||
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||||
import { BehaviorSubject, of } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CollectionAccessSelectionView,
|
||||||
|
CollectionAdminView,
|
||||||
|
Unassigned,
|
||||||
|
} from "@bitwarden/admin-console/common";
|
||||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
@ -19,13 +24,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|||||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||||
|
|
||||||
import {
|
import { GroupView } from "../../../admin-console/organizations/core";
|
||||||
CollectionAccessSelectionView,
|
|
||||||
GroupView,
|
|
||||||
} from "../../../admin-console/organizations/core";
|
|
||||||
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
||||||
import { CollectionAdminView } from "../../core/views/collection-admin.view";
|
|
||||||
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
|
||||||
|
|
||||||
import { VaultItemsComponent } from "./vault-items.component";
|
import { VaultItemsComponent } from "./vault-items.component";
|
||||||
import { VaultItemsModule } from "./vault-items.module";
|
import { VaultItemsModule } from "./vault-items.module";
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { Component, Input, OnChanges } from "@angular/core";
|
import { Component, Input, OnChanges } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
import { Unassigned } from "../vault-filter/shared/models/routed-vault-filter.model";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-org-badge",
|
selector: "app-org-badge",
|
||||||
templateUrl: "organization-name-badge.component.html",
|
templateUrl: "organization-name-badge.component.html",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user