1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-13 15:03:26 +00:00

Merge branch 'main' into ps/extension-refresh

This commit is contained in:
Victoria League
2024-09-05 11:10:33 -04:00
committed by GitHub
209 changed files with 4319 additions and 1649 deletions

View File

@@ -174,20 +174,21 @@ jobs:
with:
path: |
apps/desktop/desktop_native/napi/*.node
apps/desktop/desktop_native/dist/*
${{ env.RUNNER_TEMP }}/.cargo/registry
${{ env.RUNNER_TEMP }}/.cargo/git
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
- name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native/napi
working-directory: apps/desktop/desktop_native
env:
PKG_CONFIG_ALLOW_CROSS: true
PKG_CONFIG_ALL_STATIC: true
TARGET: musl
run: |
rustup target add x86_64-unknown-linux-musl
npm run build:cross-platform
node build.js cross-platform
- name: Build application
run: npm run dist:lin
@@ -301,13 +302,15 @@ jobs:
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: cache
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/**/*') }}
- name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native/napi
run: npm run build:cross-platform
working-directory: apps/desktop/desktop_native
run: node build.js cross-platform
- name: Build & Sign (dev)
env:
@@ -584,13 +587,15 @@ jobs:
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: cache
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/**/*') }}
- name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native/napi
run: npm run build:cross-platform
working-directory: apps/desktop/desktop_native
run: node build.js cross-platform
- name: Build application (dev)
run: npm run build
@@ -748,13 +753,15 @@ jobs:
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: cache
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/**/*') }}
- name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native/napi
run: npm run build:cross-platform
working-directory: apps/desktop/desktop_native
run: node build.js cross-platform
- name: Build
if: steps.build-cache.outputs.cache-hit != 'true'
@@ -965,13 +972,15 @@ jobs:
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: cache
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/**/*') }}
- name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native/napi
run: npm run build:cross-platform
working-directory: apps/desktop/desktop_native
run: node build.js cross-platform
- name: Build
if: steps.build-cache.outputs.cache-hit != 'true'
@@ -1168,13 +1177,15 @@ jobs:
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: cache
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/**/*') }}
- name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native/napi
run: npm run build:cross-platform
working-directory: apps/desktop/desktop_native
run: node build.js cross-platform
- name: Build
if: steps.build-cache.outputs.cache-hit != 'true'

View File

@@ -2,7 +2,7 @@
"devFlags": {},
"flags": {
"showPasswordless": true,
"enableCipherKeyEncryption": true,
"enableCipherKeyEncryption": false,
"accountSwitching": false
}
}

View File

@@ -7,7 +7,7 @@
},
"flags": {
"showPasswordless": true,
"enableCipherKeyEncryption": true,
"enableCipherKeyEncryption": false,
"accountSwitching": true
}
}

View File

@@ -1,6 +1,6 @@
{
"flags": {
"enableCipherKeyEncryption": true,
"enableCipherKeyEncryption": false,
"accountSwitching": true
}
}

View File

@@ -3477,7 +3477,7 @@
"passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": {
"message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password."
},
"logInWithPasskey": {
"logInWithPasskeyQuestion": {
"message": "Log in with passkey?"
},
"passkeyAlreadyExists": {
@@ -3489,6 +3489,9 @@
"noMatchingPasskeyLogin": {
"message": "You do not have a matching login for this site."
},
"noMatchingLoginsForSite": {
"message": "No matching logins for this site"
},
"confirm": {
"message": "Confirm"
},
@@ -3498,9 +3501,12 @@
"savePasskeyNewLogin": {
"message": "Save passkey as new login"
},
"choosePasskey": {
"chooseCipherForPasskeySave": {
"message": "Choose a login to save this passkey to"
},
"chooseCipherForPasskeyAuth": {
"message": "Choose a passkey to log in with"
},
"passkeyItem": {
"message": "Passkey Item"
},

View File

@@ -94,7 +94,7 @@
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }}
</p>
<app-fido2-use-browser-link></app-fido2-use-browser-link>
<app-fido2-use-browser-link-v1></app-fido2-use-browser-link-v1>
</ng-container>
</main>
</form>

View File

@@ -139,7 +139,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId),
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
unlockCompleted: ({ message }) => this.unlockCompleted(message),
doFullSync: () => this.updateOverlayCiphers(true),
doFullSync: () => this.updateOverlayCiphers(),
addedCipher: () => this.updateOverlayCiphers(),
addEditCipherSubmitted: () => this.updateOverlayCiphers(),
editedCipher: () => this.updateOverlayCiphers(),
@@ -272,7 +272,10 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error));
}
if (!currentTab) {
if (!currentTab || !currentTab.url?.startsWith("http")) {
if (updateAllCipherTypes) {
this.cardAndIdentityCiphers = null;
}
return;
}

View File

@@ -4,12 +4,21 @@ describe("FIDO2 page-script for manifest v2", () => {
let createdScriptElement: HTMLScriptElement;
jest.spyOn(window.document, "createElement");
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
Object.defineProperty(window.document, "contentType", { value: "text/html", writable: true });
jest.clearAllMocks();
jest.clearAllTimers();
jest.resetModules();
});
afterAll(() => {
jest.useRealTimers();
});
it("skips appending the `page-script.js` file if the document contentType is not `text/html`", () => {
Object.defineProperty(window.document, "contentType", { value: "text/plain", writable: true });
@@ -19,7 +28,7 @@ describe("FIDO2 page-script for manifest v2", () => {
});
it("appends the `page-script.js` file to the document head when the contentType is `text/html`", () => {
jest.spyOn(window.document.head, "insertBefore").mockImplementation((node) => {
jest.spyOn(window.document.head, "prepend").mockImplementation((node) => {
createdScriptElement = node as HTMLScriptElement;
return node;
});
@@ -28,16 +37,13 @@ describe("FIDO2 page-script for manifest v2", () => {
expect(window.document.createElement).toHaveBeenCalledWith("script");
expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript);
expect(window.document.head.insertBefore).toHaveBeenCalledWith(
expect.any(HTMLScriptElement),
window.document.head.firstChild,
);
expect(window.document.head.prepend).toHaveBeenCalledWith(expect.any(HTMLScriptElement));
expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`);
});
it("appends the `page-script.js` file to the document element if the head is not available", () => {
window.document.documentElement.removeChild(window.document.head);
jest.spyOn(window.document.documentElement, "insertBefore").mockImplementation((node) => {
jest.spyOn(window.document.documentElement, "prepend").mockImplementation((node) => {
createdScriptElement = node as HTMLScriptElement;
return node;
});
@@ -46,9 +52,8 @@ describe("FIDO2 page-script for manifest v2", () => {
expect(window.document.createElement).toHaveBeenCalledWith("script");
expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript);
expect(window.document.documentElement.insertBefore).toHaveBeenCalledWith(
expect(window.document.documentElement.prepend).toHaveBeenCalledWith(
expect.any(HTMLScriptElement),
window.document.documentElement.firstChild,
);
expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`);
});
@@ -63,6 +68,7 @@ describe("FIDO2 page-script for manifest v2", () => {
jest.spyOn(createdScriptElement, "remove");
createdScriptElement.dispatchEvent(new Event("load"));
jest.runAllTimers();
expect(createdScriptElement.remove).toHaveBeenCalled();
});

View File

@@ -2,18 +2,20 @@
* This script handles injection of the FIDO2 override page script into the document.
* This is required for manifest v2, but will be removed when we migrate fully to manifest v3.
*/
import { Fido2ContentScript } from "../enums/fido2-content-script.enum";
(function (globalContext) {
if (globalContext.document.contentType !== "text/html") {
return;
}
const script = globalContext.document.createElement("script");
script.src = chrome.runtime.getURL(Fido2ContentScript.PageScript);
script.addEventListener("load", () => script.remove());
script.src = chrome.runtime.getURL("content/fido2-page-script.js");
script.addEventListener("load", removeScriptOnLoad);
const scriptInsertionPoint =
globalContext.document.head || globalContext.document.documentElement;
scriptInsertionPoint.insertBefore(script, scriptInsertionPoint.firstChild);
scriptInsertionPoint.prepend(script);
function removeScriptOnLoad() {
globalThis.setTimeout(() => script?.remove(), 5000);
}
})(globalThis);

View File

@@ -2,26 +2,35 @@
* This script handles injection of the FIDO2 override page script into the document.
* This is required for manifest v2, but will be removed when we migrate fully to manifest v3.
*/
import { Fido2ContentScript } from "../enums/fido2-content-script.enum";
(function (globalContext) {
if (globalContext.document.contentType !== "text/html") {
return;
}
if (globalContext.document.readyState === "complete") {
loadScript();
const script = globalContext.document.createElement("script");
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
// within an iframe element. This prevents an issue with web mail clients that load content
// using ajax within iframes. In particular, Zimbra web mail client was observed to have this issue.
// @see https://github.com/bitwarden/clients/issues/9618
const delayScriptInjection =
globalContext.window.top !== globalContext.window &&
globalContext.document.readyState !== "complete";
if (delayScriptInjection) {
globalContext.document.addEventListener("DOMContentLoaded", injectScript);
} else {
globalContext.addEventListener("DOMContentLoaded", loadScript);
injectScript();
}
function loadScript() {
const script = globalContext.document.createElement("script");
script.src = chrome.runtime.getURL(Fido2ContentScript.PageScript);
script.addEventListener("load", () => script.remove());
function injectScript() {
const scriptInsertionPoint =
globalContext.document.head || globalContext.document.documentElement;
scriptInsertionPoint.insertBefore(script, scriptInsertionPoint.firstChild);
scriptInsertionPoint.prepend(script);
}
function removeScriptOnLoad() {
globalThis.setTimeout(() => script?.remove(), 5000);
}
})(globalThis);

View File

@@ -18,7 +18,7 @@ export enum MessageType {
}
/**
* The params provided by the page-script are created in an insecure environemnt and
* The params provided by the page-script are created in an insecure environment and
* should not be trusted. This type is used to ensure that the content-script does not
* trust the `origin` or `sameOriginWithAncestors` params.
*/
@@ -38,7 +38,7 @@ export type CredentialCreationResponse = {
};
/**
* The params provided by the page-script are created in an insecure environemnt and
* The params provided by the page-script are created in an insecure environment and
* should not be trusted. This type is used to ensure that the content-script does not
* trust the `origin` or `sameOriginWithAncestors` params.
*/

View File

@@ -0,0 +1,36 @@
<div
role="group"
appA11yTitle="{{ cipher.name }} {{ cipher.subTitle }}"
class="virtual-scroll-item"
[ngClass]="{ 'override-last': !last }"
>
<div class="box-content-row box-content-row-flex">
<button
type="button"
(click)="selectCipher(cipher)"
tabindex="0"
appStopClick
title="{{ title }} - {{ cipher.name }}"
[ngClass]="{ 'row-main': true, 'row-selected': isSelected && !isSearching }"
>
<app-vault-icon [cipher]="cipher"></app-vault-icon>
<div class="row-main-content">
<span class="text">
<span class="truncate-box">
<span class="truncate">{{ cipher.name }}</span>
<ng-container *ngIf="cipher.organizationId">
<i
class="bwi bwi-collection text-muted"
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "shared" | i18n }}</span>
</ng-container>
</span>
</span>
<span class="detail" *ngIf="getSubName(cipher)">{{ getSubName(cipher) }}</span>
<span class="detail" *ngIf="cipher.subTitle">{{ cipher.subTitle }}</span>
</div>
</button>
</div>
</div>

View File

@@ -0,0 +1,39 @@
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@Component({
selector: "app-fido2-cipher-row-v1",
templateUrl: "fido2-cipher-row-v1.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Fido2CipherRowV1Component {
@Output() onSelected = new EventEmitter<CipherView>();
@Input() cipher: CipherView;
@Input() last: boolean;
@Input() title: string;
@Input() isSearching: boolean;
@Input() isSelected: boolean;
protected selectCipher(c: CipherView) {
this.onSelected.emit(c);
}
/**
* Returns a subname for the cipher.
* If this has a FIDO2 credential, and the cipher.name is different from the FIDO2 credential's rpId, return the rpId.
* @param c Cipher
* @returns
*/
protected getSubName(c: CipherView): string | null {
const fido2Credentials = c.login?.fido2Credentials;
if (!fido2Credentials || fido2Credentials.length === 0) {
return null;
}
const [fido2Credential] = fido2Credentials;
return c.name !== fido2Credential.rpId ? fido2Credential.rpId : null;
}
}

View File

@@ -1,36 +1,21 @@
<div
role="group"
appA11yTitle="{{ cipher.name }} {{ cipher.subTitle }}"
class="virtual-scroll-item"
[ngClass]="{ 'override-last': !last }"
>
<div class="box-content-row box-content-row-flex">
<button
type="button"
(click)="selectCipher(cipher)"
tabindex="0"
appStopClick
title="{{ title }} - {{ cipher.name }}"
[ngClass]="{ 'row-main': true, 'row-selected': isSelected && !isSearching }"
>
<app-vault-icon [cipher]="cipher"></app-vault-icon>
<div class="row-main-content">
<span class="text">
<span class="truncate-box">
<span class="truncate">{{ cipher.name }}</span>
<ng-container *ngIf="cipher.organizationId">
<i
class="bwi bwi-collection text-muted"
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "shared" | i18n }}</span>
</ng-container>
</span>
</span>
<span class="detail" *ngIf="getSubName(cipher)">{{ getSubName(cipher) }}</span>
<span class="detail" *ngIf="cipher.subTitle">{{ cipher.subTitle }}</span>
</div>
</button>
</div>
</div>
<bit-item>
<button
(click)="selectCipher(cipher)"
appA11yTitle="{{ title }} - {{ cipher.name }}"
bit-item-content
tabindex="0"
type="button"
>
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
<span data-testid="item-name">
{{ cipher.name }}
<i
*ngIf="cipher.organizationId"
[appA11yTitle]="'shared' | i18n"
class="bwi bwi-collection text-muted"
></i>
</span>
<span class="detail" *ngIf="getSubName(cipher)">{{ getSubName(cipher) }}</span>
<span *ngIf="cipher.subTitle" slot="secondary">{{ cipher.subTitle }}</span>
</button>
</bit-item>

View File

@@ -1,19 +1,40 @@
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
BadgeModule,
ButtonModule,
IconButtonModule,
ItemModule,
SectionComponent,
SectionHeaderComponent,
TypographyModule,
} from "@bitwarden/components";
@Component({
selector: "app-fido2-cipher-row",
templateUrl: "fido2-cipher-row.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
BadgeModule,
ButtonModule,
CommonModule,
IconButtonModule,
ItemModule,
JslibModule,
SectionComponent,
SectionHeaderComponent,
TypographyModule,
],
})
export class Fido2CipherRowComponent {
@Output() onSelected = new EventEmitter<CipherView>();
@Input() cipher: CipherView;
@Input() last: boolean;
@Input() title: string;
@Input() isSearching: boolean;
@Input() isSelected: boolean;
protected selectCipher(c: CipherView) {
this.onSelected.emit(c);

View File

@@ -0,0 +1,52 @@
<ng-container *ngIf="(fido2PopoutSessionData$ | async).fallbackSupported">
<div class="useBrowserlink">
<button
type="button"
(click)="toggle()"
cdkOverlayOrigin
#trigger="cdkOverlayOrigin"
aria-haspopup="dialog"
aria-controls="cdk-overlay-container"
>
<span class="text-primary">
{{ "useDeviceOrHardwareKey" | i18n }}
</span>
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
</button>
</div>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="isOpen"
[cdkConnectedOverlayPositions]="overlayPosition"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
(backdropClick)="isOpen = false"
(detach)="close()"
>
<div class="box-content">
<div
class="fido2-browser-selector-dropdown"
[@transformPanel]="'open'"
cdkTrapFocus
cdkTrapFocusAutoCapture
role="dialog"
aria-modal="true"
>
<button type="button" class="fido2-browser-selector-dropdown-item" (click)="abort(false)">
<span>{{ "justOnce" | i18n }}</span>
</button>
<br />
<button type="button" class="fido2-browser-selector-dropdown-item" (click)="abort()">
<span>{{ "alwaysForThisSite" | i18n }}</span>
</button>
</div>
</div>
</ng-template>
<div
*ngIf="showOverlay"
class="tw-absolute tw-w-full tw-h-full tw-bg-background-alt tw-inset-0 tw-bg-opacity-80 tw-z-50"
></div>
</ng-container>

View File

@@ -0,0 +1,113 @@
import { animate, state, style, transition, trigger } from "@angular/animations";
import { ConnectedPosition } from "@angular/cdk/overlay";
import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data";
import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service";
@Component({
selector: "app-fido2-use-browser-link-v1",
templateUrl: "fido2-use-browser-link-v1.component.html",
animations: [
trigger("transformPanel", [
state(
"void",
style({
opacity: 0,
}),
),
transition(
"void => open",
animate(
"100ms linear",
style({
opacity: 1,
}),
),
),
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
]),
],
})
export class Fido2UseBrowserLinkV1Component {
showOverlay = false;
isOpen = false;
overlayPosition: ConnectedPosition[] = [
{
originX: "start",
originY: "bottom",
overlayX: "start",
overlayY: "top",
offsetY: 5,
},
];
protected fido2PopoutSessionData$ = fido2PopoutSessionData$();
constructor(
private domainSettingsService: DomainSettingsService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
) {}
toggle() {
this.isOpen = !this.isOpen;
}
close() {
this.isOpen = false;
}
/**
* Aborts the current FIDO2 session and fallsback to the browser.
* @param excludeDomain - Identifies if the domain should be excluded from future FIDO2 prompts.
*/
protected async abort(excludeDomain = true) {
this.close();
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
if (!excludeDomain) {
this.abortSession(sessionData.sessionId);
return;
}
// Show overlay to prevent the user from interacting with the page.
this.showOverlay = true;
await this.handleDomainExclusion(sessionData.senderUrl);
// Give the user a chance to see the toast before closing the popout.
await Utils.delay(2000);
this.abortSession(sessionData.sessionId);
}
/**
* Excludes the domain from future FIDO2 prompts.
* @param uri - The domain uri to exclude from future FIDO2 prompts.
*/
private async handleDomainExclusion(uri: string) {
const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
const validDomain = Utils.getHostname(uri);
const savedDomains: NeverDomains = {
...existingDomains,
};
savedDomains[validDomain] = null;
await this.domainSettingsService.setNeverDomains(savedDomains);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("domainAddedToExcludedDomains", validDomain),
);
}
private abortSession(sessionId: string) {
BrowserFido2UserInterfaceSession.abortPopout(sessionId, true);
}
}

View File

@@ -1,8 +1,11 @@
import { animate, state, style, transition, trigger } from "@angular/animations";
import { ConnectedPosition } from "@angular/cdk/overlay";
import { A11yModule } from "@angular/cdk/a11y";
import { ConnectedPosition, CdkOverlayOrigin, CdkConnectedOverlay } from "@angular/cdk/overlay";
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -15,6 +18,8 @@ import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-f
@Component({
selector: "app-fido2-use-browser-link",
templateUrl: "fido2-use-browser-link.component.html",
standalone: true,
imports: [A11yModule, CdkConnectedOverlay, CdkOverlayOrigin, CommonModule, JslibModule],
animations: [
trigger("transformPanel", [
state(
@@ -90,11 +95,11 @@ export class Fido2UseBrowserLinkComponent {
* @param uri - The domain uri to exclude from future FIDO2 prompts.
*/
private async handleDomainExclusion(uri: string) {
const exisitingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
const validDomain = Utils.getHostname(uri);
const savedDomains: NeverDomains = {
...exisitingDomains,
...existingDomains,
};
savedDomains[validDomain] = null;

View File

@@ -0,0 +1,142 @@
<ng-container *ngIf="data$ | async as data">
<div class="auth-wrapper">
<div class="auth-header">
<div class="left">
<ng-container *ngIf="data.message.type != BrowserFido2MessageTypes.PickCredentialRequest">
<div class="logo">
<i class="bwi bwi-shield"></i>
</div>
</ng-container>
<ng-container *ngIf="data.message.type === BrowserFido2MessageTypes.PickCredentialRequest">
<div class="logo">
<i class="bwi bwi-shield"></i><span><strong>bit</strong>warden</span>
</div>
</ng-container>
</div>
<ng-container
*ngIf="data.message.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest"
>
<div class="search">
<input
type="{{ searchTypeSearch ? 'search' : 'text' }}"
placeholder="{{ 'searchVault' | i18n }}"
id="search"
[(ngModel)]="searchText"
(input)="search()"
autocomplete="off"
appAutofocus
/>
<i class="bwi bwi-search" aria-hidden="true"></i>
</div>
<div class="right">
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</ng-container>
</div>
<ng-container>
<ng-container
*ngIf="
data.message.type === BrowserFido2MessageTypes.PickCredentialRequest ||
data.message.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest
"
>
<div class="auth-flow">
<p class="subtitle" appA11yTitle="{{ subtitleText | i18n }}">
{{ subtitleText | i18n }}
</p>
<!-- Display when ciphers exist -->
<ng-container *ngIf="displayedCiphers.length > 0">
<div class="box list">
<div class="box-content">
<app-fido2-cipher-row-v1
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
[isSearching]="searchPending"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="selectedPasskey($event)"
[isSelected]="cipher === cipherItem"
></app-fido2-cipher-row-v1>
</div>
</div>
<div class="box">
<button
type="submit"
(click)="submit()"
class="btn primary block"
appA11yTitle="{{ credentialText | i18n }}"
>
<span [hidden]="loading">
{{ credentialText | i18n }}
</span>
<i
class="bwi bwi-spinner bwi-lg bwi-spin"
[hidden]="!loading"
aria-hidden="true"
></i>
</button>
</div>
</ng-container>
<ng-container *ngIf="!displayedCiphers.length">
<div class="box">
<button
type="submit"
(click)="saveNewLogin()"
class="btn primary block"
appA11yTitle="{{ 'savePasskeyNewLogin' | i18n }}"
>
<span [hidden]="loading">
{{ "savePasskeyNewLogin" | i18n }}
</span>
<i
class="bwi bwi-spinner bwi-lg bwi-spin"
[hidden]="!loading"
aria-hidden="true"
></i>
</button>
</div>
</ng-container>
</div>
</ng-container>
<ng-container
*ngIf="data.message.type === BrowserFido2MessageTypes.InformExcludedCredentialRequest"
>
<div class="auth-flow">
<p class="subtitle">{{ "passkeyAlreadyExists" | i18n }}</p>
<div class="box list">
<div class="box-content">
<app-fido2-cipher-row-v1
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="selectedPasskey($event)"
[isSelected]="cipher === cipherItem"
></app-fido2-cipher-row-v1>
</div>
</div>
<button type="button" class="btn primary block" (click)="viewPasskey()">
<span [hidden]="loading">{{ "viewItem" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
</button>
</div>
</ng-container>
<ng-container
*ngIf="data.message.type === BrowserFido2MessageTypes.InformCredentialNotFoundRequest"
>
<div class="auth-flow">
<p class="subtitle">{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
</div>
<button type="button" class="btn primary block" (click)="abort(false)">
<span [hidden]="loading">{{ "close" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
</button>
</ng-container>
</ng-container>
<app-fido2-use-browser-link-v1></app-fido2-use-browser-link-v1>
</div>
</ng-container>

View File

@@ -0,0 +1,443 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import {
BehaviorSubject,
combineLatest,
concatMap,
filter,
firstValueFrom,
map,
Observable,
Subject,
take,
takeUntil,
} from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service";
import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window";
import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service";
import {
BrowserFido2Message,
BrowserFido2UserInterfaceSession,
BrowserFido2MessageTypes,
} from "../../fido2/services/browser-fido2-user-interface.service";
interface ViewData {
message: BrowserFido2Message;
fallbackSupported: boolean;
}
@Component({
selector: "app-fido2-v1",
templateUrl: "fido2-v1.component.html",
styleUrls: [],
})
export class Fido2V1Component implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
private hasSearched = false;
protected cipher: CipherView;
protected searchTypeSearch = false;
protected searchPending = false;
protected searchText: string;
protected url: string;
protected hostname: string;
protected data$: Observable<ViewData>;
protected sessionId?: string;
protected senderTabId?: string;
protected ciphers?: CipherView[] = [];
protected displayedCiphers?: CipherView[] = [];
protected loading = false;
protected subtitleText: string;
protected credentialText: string;
protected BrowserFido2MessageTypes = BrowserFido2MessageTypes;
private message$ = new BehaviorSubject<BrowserFido2Message>(null);
constructor(
private router: Router,
private activatedRoute: ActivatedRoute,
private cipherService: CipherService,
private platformUtilsService: PlatformUtilsService,
private domainSettingsService: DomainSettingsService,
private searchService: SearchService,
private logService: LogService,
private dialogService: DialogService,
private browserMessagingApi: ZonedMessageListenerService,
private passwordRepromptService: PasswordRepromptService,
private fido2UserVerificationService: Fido2UserVerificationService,
private accountService: AccountService,
) {}
ngOnInit() {
this.searchTypeSearch = !this.platformUtilsService.isSafari();
const queryParams$ = this.activatedRoute.queryParamMap.pipe(
take(1),
map((queryParamMap) => ({
sessionId: queryParamMap.get("sessionId"),
senderTabId: queryParamMap.get("senderTabId"),
senderUrl: queryParamMap.get("senderUrl"),
})),
);
combineLatest([
queryParams$,
this.browserMessagingApi.messageListener$() as Observable<BrowserFido2Message>,
])
.pipe(
concatMap(async ([queryParams, message]) => {
this.sessionId = queryParams.sessionId;
this.senderTabId = queryParams.senderTabId;
this.url = queryParams.senderUrl;
// For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
if (
message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest &&
message.sessionId !== queryParams.sessionId
) {
this.abort(false);
return;
}
// Ignore messages that don't belong to the current session.
if (message.sessionId !== queryParams.sessionId) {
return;
}
if (message.type === BrowserFido2MessageTypes.AbortRequest) {
this.abort(false);
return;
}
return message;
}),
filter((message) => !!message),
takeUntil(this.destroy$),
)
.subscribe((message) => {
this.message$.next(message);
});
this.data$ = this.message$.pipe(
filter((message) => message != undefined),
concatMap(async (message) => {
switch (message.type) {
case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: {
const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
);
this.displayedCiphers = this.ciphers.filter(
(cipher) =>
cipher.login.matchesUri(this.url, equivalentDomains) &&
this.hasNoOtherPasskeys(cipher, message.userHandle),
);
if (this.displayedCiphers.length > 0) {
this.selectedPasskey(this.displayedCiphers[0]);
}
break;
}
case BrowserFido2MessageTypes.PickCredentialRequest: {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.ciphers = await Promise.all(
message.cipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId);
return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
}),
);
this.displayedCiphers = [...this.ciphers];
if (this.displayedCiphers.length > 0) {
this.selectedPasskey(this.displayedCiphers[0]);
}
break;
}
case BrowserFido2MessageTypes.InformExcludedCredentialRequest: {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.ciphers = await Promise.all(
message.existingCipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId);
return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
}),
);
this.displayedCiphers = [...this.ciphers];
if (this.displayedCiphers.length > 0) {
this.selectedPasskey(this.displayedCiphers[0]);
}
break;
}
}
this.subtitleText =
this.displayedCiphers.length > 0
? this.getCredentialSubTitleText(message.type)
: "noMatchingPasskeyLogin";
this.credentialText = this.getCredentialButtonText(message.type);
return {
message,
fallbackSupported: "fallbackSupported" in message && message.fallbackSupported,
};
}),
takeUntil(this.destroy$),
);
queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => {
this.send({
sessionId: queryParams.sessionId,
type: BrowserFido2MessageTypes.ConnectResponse,
});
});
}
protected async submit() {
const data = this.message$.value;
if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) {
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
this.send({
sessionId: this.sessionId,
cipherId: this.cipher.id,
type: BrowserFido2MessageTypes.PickCredentialResponse,
userVerified,
});
} else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
if (this.cipher.login.hasFido2Credentials) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "overwritePasskey" },
content: { key: "overwritePasskeyAlert" },
type: "info",
});
if (!confirmed) {
return false;
}
}
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
this.send({
sessionId: this.sessionId,
cipherId: this.cipher.id,
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
userVerified,
});
}
this.loading = true;
}
protected async saveNewLogin() {
const data = this.message$.value;
if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
const name = data.credentialName || data.rpId;
// TODO: Revert to check for user verification once user verification for passkeys is approved for production.
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
await this.createNewCipher(name, data.userName);
// We are bypassing user verification pending approval.
this.send({
sessionId: this.sessionId,
cipherId: this.cipher?.id,
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
userVerified: data.userVerification,
});
}
this.loading = true;
}
getCredentialSubTitleText(messageType: string): string {
return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest
? "chooseCipherForPasskeySave"
: "logInWithPasskeyQuestion";
}
getCredentialButtonText(messageType: string): string {
return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest
? "savePasskey"
: "confirm";
}
selectedPasskey(item: CipherView) {
this.cipher = item;
}
viewPasskey() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/view-cipher"], {
queryParams: {
cipherId: this.cipher.id,
uilocation: "popout",
senderTabId: this.senderTabId,
sessionId: this.sessionId,
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
},
});
}
addCipher() {
const data = this.message$.value;
if (data?.type !== BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
return;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/add-cipher"], {
queryParams: {
name: data.credentialName || data.rpId,
uri: this.url,
type: CipherType.Login.toString(),
uilocation: "popout",
username: data.userName,
senderTabId: this.senderTabId,
sessionId: this.sessionId,
userVerification: data.userVerification,
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
},
});
}
protected async search() {
this.hasSearched = await this.searchService.isSearchable(this.searchText);
this.searchPending = true;
if (this.hasSearched) {
this.displayedCiphers = await this.searchService.searchCiphers(
this.searchText,
null,
this.ciphers,
);
} else {
const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
this.displayedCiphers = this.ciphers.filter((cipher) =>
cipher.login.matchesUri(this.url, equivalentDomains),
);
}
this.searchPending = false;
this.selectedPasskey(this.displayedCiphers[0]);
}
abort(fallback: boolean) {
this.unload(fallback);
window.close();
}
unload(fallback = false) {
this.send({
sessionId: this.sessionId,
type: BrowserFido2MessageTypes.AbortResponse,
fallbackRequested: fallback,
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private buildCipher(name: string, username: string) {
this.cipher = new CipherView();
this.cipher.name = name;
this.cipher.type = CipherType.Login;
this.cipher.login = new LoginView();
this.cipher.login.username = username;
this.cipher.login.uris = [new LoginUriView()];
this.cipher.login.uris[0].uri = this.url;
this.cipher.card = new CardView();
this.cipher.identity = new IdentityView();
this.cipher.secureNote = new SecureNoteView();
this.cipher.secureNote.type = SecureNoteType.Generic;
this.cipher.reprompt = CipherRepromptType.None;
}
private async createNewCipher(name: string, username: string) {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.buildCipher(name, username);
const cipher = await this.cipherService.encrypt(this.cipher, activeUserId);
try {
await this.cipherService.createWithServer(cipher);
this.cipher.id = cipher.id;
} catch (e) {
this.logService.error(e);
}
}
// TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production.
private async handleUserVerification(
userVerificationRequested: boolean,
cipher: CipherView,
): Promise<boolean> {
const masterPasswordRepromptRequired = cipher && cipher.reprompt !== 0;
if (masterPasswordRepromptRequired) {
return await this.passwordRepromptService.showPasswordPrompt();
}
return userVerificationRequested;
}
private send(msg: BrowserFido2Message) {
BrowserFido2UserInterfaceSession.sendMessage({
sessionId: this.sessionId,
...msg,
});
}
/**
* This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle
* @param userHandle
*/
private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean {
if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) {
return true;
}
return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle);
}
}

View File

@@ -1,136 +1,134 @@
<ng-container *ngIf="data$ | async as data">
<div class="auth-wrapper">
<div class="auth-header">
<div class="left">
<ng-container *ngIf="data.message.type != 'PickCredentialRequest'">
<div class="logo">
<i class="bwi bwi-shield"></i>
<popup-page *ngIf="data$ | async as data">
<popup-header
slot="header"
pageTitle="{{
(passkeyAction === PasskeyActions.Register ? 'savePasskey' : 'logInWithPasskeyQuestion')
| i18n
}}"
>
<button
*ngIf="showNewPasskeyButton"
bitButton
buttonType="primary"
type="button"
(click)="addCipher()"
slot="end"
>
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ "new" | i18n }}
</button>
</popup-header>
<div class="tw-p-2">
<bit-section *ngIf="passkeyAction === PasskeyActions.Register">
<bit-search
appAutofocus
autocomplete="off"
id="search"
placeholder="{{ 'searchVault' | i18n }}"
(ngModelChange)="search()"
[(ngModel)]="searchText"
></bit-search>
</bit-section>
<!-- Display when adding a new passkey -->
<bit-section *ngIf="data.message.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest">
<!-- Display when matching ciphers (i.e. same domain, no passkeys) exist -->
<ng-container *ngIf="displayedCiphers.length > 0">
<bit-section-header>
<h2 bitTypography="h6">{{ "chooseCipherForPasskeySave" | i18n }}</h2>
</bit-section-header>
<app-fido2-cipher-row
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="handleCipherItemSelect($event)"
></app-fido2-cipher-row>
</ng-container>
<!-- Display when no matching ciphers exist -->
<ng-container *ngIf="!displayedCiphers.length">
<bit-no-items class="tw-text-main" [icon]="noResultsIcon">
<ng-container slot="title">{{ "noMatchingLoginsForSite" | i18n }}</ng-container>
<ng-container slot="description">Search or save passkey as new login</ng-container>
<button
bitButton
buttonType="primary"
slot="button"
type="button"
(click)="saveNewLogin()"
[loading]="loading"
>
{{ "savePasskeyNewLogin" | i18n }}
</button>
</bit-no-items>
</ng-container>
</bit-section>
<!-- Display when the passkey being saved already exists -->
<bit-section
*ngIf="data.message.type === BrowserFido2MessageTypes.InformExcludedCredentialRequest"
>
<div class="auth-flow">
<p class="subtitle">{{ "passkeyAlreadyExists" | i18n }}</p>
<div class="box list">
<div class="box-content">
<app-fido2-cipher-row
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="handleCipherItemSelect($event)"
></app-fido2-cipher-row>
</div>
</ng-container>
<ng-container *ngIf="data.message.type === 'PickCredentialRequest'">
<div class="logo">
<i class="bwi bwi-shield"></i><span><strong>bit</strong>warden</span>
</div>
</ng-container>
</div>
</div>
<ng-container *ngIf="data.message.type === 'ConfirmNewCredentialRequest'">
<div class="search">
<input
type="{{ searchTypeSearch ? 'search' : 'text' }}"
placeholder="{{ 'searchVault' | i18n }}"
id="search"
[(ngModel)]="searchText"
(input)="search()"
autocomplete="off"
appAutofocus
/>
<i class="bwi bwi-search" aria-hidden="true"></i>
</div>
<div class="right">
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</bit-section>
<!-- Display when picking a passkey to login with -->
<bit-section *ngIf="data.message.type === BrowserFido2MessageTypes.PickCredentialRequest">
<!-- Display when matching ciphers exist -->
<ng-container *ngIf="displayedCiphers.length > 0">
<ng-container slot="title">{{ "chooseCipherForPasskeyAuth" | i18n }}</ng-container>
<app-fido2-cipher-row
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="handleCipherItemSelect($event)"
></app-fido2-cipher-row>
</ng-container>
<!-- Display when no matching ciphers exist -->
<ng-container *ngIf="!displayedCiphers.length">
<bit-no-items class="tw-text-main" [icon]="noResultsIcon">
<ng-container slot="title">No matching logins for this site</ng-container>
<ng-container slot="description">Search or save passkey as new login</ng-container>
<button
bitButton
buttonType="primary"
slot="button"
type="button"
(click)="saveNewLogin()"
[loading]="loading"
>
{{ "savePasskeyNewLogin" | i18n }}
</button>
</div>
</bit-no-items>
</ng-container>
</div>
</bit-section>
<ng-container>
<ng-container
*ngIf="
data.message.type === 'PickCredentialRequest' ||
data.message.type === 'ConfirmNewCredentialRequest'
"
>
<div class="auth-flow">
<p class="subtitle" appA11yTitle="{{ subtitleText | i18n }}">
{{ subtitleText | i18n }}
</p>
<!-- Display when ciphers exist -->
<ng-container *ngIf="displayedCiphers.length > 0">
<div class="box list">
<div class="box-content">
<app-fido2-cipher-row
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
[isSearching]="searchPending"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="selectedPasskey($event)"
[isSelected]="cipher === cipherItem"
></app-fido2-cipher-row>
</div>
</div>
<div class="box">
<button
type="submit"
(click)="submit()"
class="btn primary block"
appA11yTitle="{{ credentialText | i18n }}"
>
<span [hidden]="loading">
{{ credentialText | i18n }}
</span>
<i
class="bwi bwi-spinner bwi-lg bwi-spin"
[hidden]="!loading"
aria-hidden="true"
></i>
</button>
</div>
</ng-container>
<ng-container *ngIf="!displayedCiphers.length">
<div class="box">
<button
type="submit"
(click)="saveNewLogin()"
class="btn primary block"
appA11yTitle="{{ 'savePasskeyNewLogin' | i18n }}"
>
<span [hidden]="loading">
{{ "savePasskeyNewLogin" | i18n }}
</span>
<i
class="bwi bwi-spinner bwi-lg bwi-spin"
[hidden]="!loading"
aria-hidden="true"
></i>
</button>
</div>
</ng-container>
</div>
</ng-container>
<ng-container *ngIf="data.message.type === 'InformExcludedCredentialRequest'">
<div class="auth-flow">
<p class="subtitle">{{ "passkeyAlreadyExists" | i18n }}</p>
<div class="box list">
<div class="box-content">
<app-fido2-cipher-row
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="selectedPasskey($event)"
[isSelected]="cipher === cipherItem"
></app-fido2-cipher-row>
</div>
</div>
<button type="button" class="btn primary block" (click)="viewPasskey()">
<span [hidden]="loading">{{ "viewItem" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
</button>
</div>
</ng-container>
<ng-container *ngIf="data.message.type === 'InformCredentialNotFoundRequest'">
<div class="auth-flow">
<p class="subtitle">{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
</div>
<button type="button" class="btn primary block" (click)="abort(false)">
<span [hidden]="loading">{{ "close" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
</button>
</ng-container>
</ng-container>
<!-- Display when initiating passkey login, but no cooresponding cipher is found in the vault -->
<bit-section
*ngIf="data.message.type === BrowserFido2MessageTypes.InformCredentialNotFoundRequest"
>
<div class="auth-flow">
<p class="subtitle">{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
</div>
<button type="button" class="btn primary block" (click)="abort(false)">
<span [hidden]="loading">{{ "close" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
</button>
</bit-section>
<app-fido2-use-browser-link></app-fido2-use-browser-link>
</div>
</ng-container>
</popup-page>

View File

@@ -1,4 +1,6 @@
import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import {
BehaviorSubject,
@@ -13,13 +15,14 @@ import {
takeUntil,
} from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
import { SecureNoteType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -27,17 +30,39 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
import { DialogService } from "@bitwarden/components";
import {
ButtonModule,
DialogService,
Icons,
ItemModule,
NoItemsModule,
SearchModule,
SectionComponent,
SectionHeaderComponent,
} from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window";
import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service";
import {
BrowserFido2Message,
BrowserFido2UserInterfaceSession,
BrowserFido2MessageTypes,
} from "../../fido2/services/browser-fido2-user-interface.service";
import { Fido2CipherRowComponent } from "./fido2-cipher-row.component";
import { Fido2UseBrowserLinkComponent } from "./fido2-use-browser-link.component";
const PasskeyActions = {
Register: "register",
Authenticate: "authenticate",
} as const;
type PasskeyActionValue = (typeof PasskeyActions)[keyof typeof PasskeyActions];
interface ViewData {
message: BrowserFido2Message;
fallbackSupported: boolean;
@@ -46,28 +71,45 @@ interface ViewData {
@Component({
selector: "app-fido2",
templateUrl: "fido2.component.html",
styleUrls: [],
standalone: true,
imports: [
ButtonModule,
CommonModule,
Fido2CipherRowComponent,
Fido2UseBrowserLinkComponent,
FormsModule,
ItemModule,
JslibModule,
NoItemsModule,
PopupHeaderComponent,
PopupPageComponent,
SearchModule,
SectionComponent,
SectionHeaderComponent,
],
})
export class Fido2Component implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
private hasSearched = false;
protected cipher: CipherView;
protected searchTypeSearch = false;
protected searchPending = false;
protected searchText: string;
protected url: string;
protected hostname: string;
protected data$: Observable<ViewData>;
protected sessionId?: string;
protected senderTabId?: string;
protected ciphers?: CipherView[] = [];
protected displayedCiphers?: CipherView[] = [];
protected loading = false;
protected subtitleText: string;
protected credentialText: string;
private message$ = new BehaviorSubject<BrowserFido2Message>(null);
private hasSearched = false;
protected BrowserFido2MessageTypes = BrowserFido2MessageTypes;
protected cipher: CipherView;
protected ciphers?: CipherView[] = [];
protected data$: Observable<ViewData>;
protected displayedCiphers?: CipherView[] = [];
protected equivalentDomains: Set<string>;
protected equivalentDomainsURL: string;
protected hostname: string;
protected loading = false;
protected noResultsIcon = Icons.NoResults;
protected passkeyAction: PasskeyActionValue = PasskeyActions.Register;
protected PasskeyActions = PasskeyActions;
protected searchText: string;
protected searchTypeSearch = false;
protected senderTabId?: string;
protected sessionId?: string;
protected showNewPasskeyButton: boolean = false;
protected url: string;
constructor(
private router: Router,
@@ -80,8 +122,8 @@ export class Fido2Component implements OnInit, OnDestroy {
private dialogService: DialogService,
private browserMessagingApi: ZonedMessageListenerService,
private passwordRepromptService: PasswordRepromptService,
private fido2UserVerificationService: Fido2UserVerificationService,
private accountService: AccountService,
private fido2UserVerificationService: Fido2UserVerificationService,
) {}
ngOnInit() {
@@ -107,7 +149,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.url = queryParams.senderUrl;
// For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
if (
message.type === "NewSessionCreatedRequest" &&
message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest &&
message.sessionId !== queryParams.sessionId
) {
this.abort(false);
@@ -119,7 +161,7 @@ export class Fido2Component implements OnInit, OnDestroy {
return;
}
if (message.type === "AbortRequest") {
if (message.type === BrowserFido2MessageTypes.AbortRequest) {
this.abort(false);
return;
}
@@ -137,7 +179,7 @@ export class Fido2Component implements OnInit, OnDestroy {
filter((message) => message != undefined),
concatMap(async (message) => {
switch (message.type) {
case "ConfirmNewCredentialRequest": {
case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: {
const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
@@ -145,19 +187,22 @@ export class Fido2Component implements OnInit, OnDestroy {
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
);
this.displayedCiphers = this.ciphers.filter(
(cipher) =>
cipher.login.matchesUri(this.url, equivalentDomains) &&
this.hasNoOtherPasskeys(cipher, message.userHandle),
this.cipherHasNoOtherPasskeys(cipher, message.userHandle),
);
if (this.displayedCiphers.length > 0) {
this.selectedPasskey(this.displayedCiphers[0]);
}
this.passkeyAction = PasskeyActions.Register;
// @TODO fix new cipher creation for other fido2 registration message types and remove `showNewPasskeyButton` from the template
this.showNewPasskeyButton = true;
break;
}
case "PickCredentialRequest": {
case BrowserFido2MessageTypes.PickCredentialRequest: {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
@@ -170,14 +215,15 @@ export class Fido2Component implements OnInit, OnDestroy {
);
}),
);
this.displayedCiphers = [...this.ciphers];
if (this.displayedCiphers.length > 0) {
this.selectedPasskey(this.displayedCiphers[0]);
}
this.passkeyAction = PasskeyActions.Authenticate;
break;
}
case "InformExcludedCredentialRequest": {
case BrowserFido2MessageTypes.InformExcludedCredentialRequest: {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
@@ -190,40 +236,42 @@ export class Fido2Component implements OnInit, OnDestroy {
);
}),
);
this.displayedCiphers = [...this.ciphers];
if (this.displayedCiphers.length > 0) {
this.selectedPasskey(this.displayedCiphers[0]);
}
this.passkeyAction = PasskeyActions.Register;
break;
}
case BrowserFido2MessageTypes.InformCredentialNotFoundRequest: {
this.passkeyAction = PasskeyActions.Authenticate;
break;
}
}
this.subtitleText =
this.displayedCiphers.length > 0
? this.getCredentialSubTitleText(message.type)
: "noMatchingPasskeyLogin";
this.credentialText = this.getCredentialButtonText(message.type);
return {
message,
fallbackSupported: "fallbackSupported" in message && message.fallbackSupported,
};
}),
takeUntil(this.destroy$),
);
queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => {
this.send({
sessionId: queryParams.sessionId,
type: "ConnectResponse",
type: BrowserFido2MessageTypes.ConnectResponse,
});
});
}
protected async submit() {
const data = this.message$.value;
if (data?.type === "PickCredentialRequest") {
if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) {
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
@@ -231,10 +279,10 @@ export class Fido2Component implements OnInit, OnDestroy {
this.send({
sessionId: this.sessionId,
cipherId: this.cipher.id,
type: "PickCredentialResponse",
type: BrowserFido2MessageTypes.PickCredentialResponse,
userVerified,
});
} else if (data?.type === "ConfirmNewCredentialRequest") {
} else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
if (this.cipher.login.hasFido2Credentials) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "overwritePasskey" },
@@ -254,7 +302,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.send({
sessionId: this.sessionId,
cipherId: this.cipher.id,
type: "ConfirmNewCredentialResponse",
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
userVerified,
});
}
@@ -264,7 +312,8 @@ export class Fido2Component implements OnInit, OnDestroy {
protected async saveNewLogin() {
const data = this.message$.value;
if (data?.type === "ConfirmNewCredentialRequest") {
if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
const name = data.credentialName || data.rpId;
// TODO: Revert to check for user verification once user verification for passkeys is approved for production.
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
@@ -274,7 +323,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.send({
sessionId: this.sessionId,
cipherId: this.cipher?.id,
type: "ConfirmNewCredentialResponse",
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
userVerified: data.userVerification,
});
}
@@ -282,59 +331,47 @@ export class Fido2Component implements OnInit, OnDestroy {
this.loading = true;
}
getCredentialSubTitleText(messageType: string): string {
return messageType == "ConfirmNewCredentialRequest" ? "choosePasskey" : "logInWithPasskey";
}
getCredentialButtonText(messageType: string): string {
return messageType == "ConfirmNewCredentialRequest" ? "savePasskey" : "confirm";
}
selectedPasskey(item: CipherView) {
async handleCipherItemSelect(item: CipherView) {
this.cipher = item;
await this.submit();
}
viewPasskey() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/view-cipher"], {
queryParams: {
cipherId: this.cipher.id,
uilocation: "popout",
senderTabId: this.senderTabId,
sessionId: this.sessionId,
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
},
});
}
addCipher() {
async addCipher() {
const data = this.message$.value;
if (data?.type !== "ConfirmNewCredentialRequest") {
return;
if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
await this.router.navigate(["/add-cipher"], {
queryParams: {
type: CipherType.Login.toString(),
name: data.credentialName || data.rpId,
uri: this.url,
uilocation: "popout",
username: data.userName,
senderTabId: this.senderTabId,
sessionId: this.sessionId,
userVerification: data.userVerification,
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
},
});
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/add-cipher"], {
queryParams: {
name: data.credentialName || data.rpId,
uri: this.url,
type: CipherType.Login.toString(),
uilocation: "popout",
username: data.userName,
senderTabId: this.senderTabId,
sessionId: this.sessionId,
userVerification: data.userVerification,
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
},
});
return;
}
async getEquivalentDomains() {
if (this.equivalentDomainsURL !== this.url) {
this.equivalentDomainsURL = this.url;
this.equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
}
return this.equivalentDomains;
}
protected async search() {
this.hasSearched = await this.searchService.isSearchable(this.searchText);
this.searchPending = true;
if (this.hasSearched) {
this.displayedCiphers = await this.searchService.searchCiphers(
this.searchText,
@@ -342,15 +379,11 @@ export class Fido2Component implements OnInit, OnDestroy {
this.ciphers,
);
} else {
const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
const equivalentDomains = await this.getEquivalentDomains();
this.displayedCiphers = this.ciphers.filter((cipher) =>
cipher.login.matchesUri(this.url, equivalentDomains),
);
}
this.searchPending = false;
this.selectedPasskey(this.displayedCiphers[0]);
}
abort(fallback: boolean) {
@@ -361,7 +394,7 @@ export class Fido2Component implements OnInit, OnDestroy {
unload(fallback = false) {
this.send({
sessionId: this.sessionId,
type: "AbortResponse",
type: BrowserFido2MessageTypes.AbortResponse,
fallbackRequested: fallback,
});
}
@@ -427,13 +460,11 @@ export class Fido2Component implements OnInit, OnDestroy {
* This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle
* @param userHandle
*/
private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean {
private cipherHasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean {
if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) {
return true;
}
return cipher.login.fido2Credentials.some((passkey) => {
passkey.userHandle === userHandle;
});
return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle);
}
}

View File

@@ -122,29 +122,9 @@ export class InlineMenuFieldQualificationService
...this.identityAddressAutoCompleteValues,
...this.identityCountryAutocompleteValues,
...this.identityPhoneNumberAutocompleteValues,
this.identityCompanyAutocompleteValue,
this.identityPostalCodeAutocompleteValue,
]);
private identityFieldKeywords = [
...new Set([
...IdentityAutoFillConstants.TitleFieldNames,
...IdentityAutoFillConstants.FullNameFieldNames,
...IdentityAutoFillConstants.FirstnameFieldNames,
...IdentityAutoFillConstants.MiddlenameFieldNames,
...IdentityAutoFillConstants.LastnameFieldNames,
...IdentityAutoFillConstants.AddressFieldNames,
...IdentityAutoFillConstants.Address1FieldNames,
...IdentityAutoFillConstants.Address2FieldNames,
...IdentityAutoFillConstants.Address3FieldNames,
...IdentityAutoFillConstants.PostalCodeFieldNames,
...IdentityAutoFillConstants.CityFieldNames,
...IdentityAutoFillConstants.StateFieldNames,
...IdentityAutoFillConstants.CountryFieldNames,
...IdentityAutoFillConstants.CompanyFieldNames,
...IdentityAutoFillConstants.PhoneFieldNames,
...IdentityAutoFillConstants.EmailFieldNames,
...IdentityAutoFillConstants.UserNameFieldNames,
]),
];
private inlineMenuFieldQualificationFlagSet = false;
constructor() {
@@ -288,14 +268,7 @@ export class InlineMenuFieldQualificationService
return false;
}
if (this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, this.identityFieldKeywords, false)
);
return this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues);
}
/**

View File

@@ -1267,6 +1267,18 @@ export default class MainBackground {
);
}
// If the user is logged out, switch to the next account
const active = await firstValueFrom(this.accountService.activeAccount$);
if (active != null) {
const authStatus = await firstValueFrom(
this.authService.authStatuses$.pipe(map((statuses) => statuses[active.id])),
);
if (authStatus === AuthenticationStatus.LoggedOut) {
const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$);
await this.switchAccount(nextUpAccount?.id);
}
}
await this.initOverlayAndTabsBackground();
return new Promise<void>((resolve) => {

View File

@@ -41,6 +41,7 @@ import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component"
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component";
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
@@ -127,12 +128,11 @@ const routes: Routes = [
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: { state: "home" },
},
{
...extensionRefreshSwap(Fido2V1Component, Fido2Component, {
path: "fido2",
component: Fido2Component,
canActivate: [fido2AuthGuard],
data: { state: "fido2" },
},
}),
{
path: "login",
component: LoginComponent,
@@ -304,7 +304,6 @@ const routes: Routes = [
},
...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, {
path: "notifications",
component: NotificationsSettingsV1Component,
canActivate: [authGuard],
data: { state: "notifications" },
}),
@@ -338,7 +337,6 @@ const routes: Routes = [
},
...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, {
path: "excluded-domains",
component: ExcludedDomainsV1Component,
canActivate: [authGuard],
data: { state: "excluded-domains" },
}),

View File

@@ -35,8 +35,11 @@ import { SsoComponent } from "../auth/popup/sso.component";
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { Fido2CipherRowV1Component } from "../autofill/popup/fido2/fido2-cipher-row-v1.component";
import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component";
import { Fido2UseBrowserLinkV1Component } from "../autofill/popup/fido2/fido2-use-browser-link-v1.component";
import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component";
import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component";
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
@@ -112,6 +115,9 @@ import "../platform/popup/locales";
ServicesModule,
DialogModule,
ExcludedDomainsComponent,
Fido2CipherRowComponent,
Fido2Component,
Fido2UseBrowserLinkComponent,
FilePopoutCalloutComponent,
AvatarModule,
AccountComponent,
@@ -140,8 +146,8 @@ import "../platform/popup/locales";
CurrentTabComponent,
EnvironmentComponent,
ExcludedDomainsV1Component,
Fido2CipherRowComponent,
Fido2UseBrowserLinkComponent,
Fido2CipherRowV1Component,
Fido2UseBrowserLinkV1Component,
FolderAddEditComponent,
FoldersComponent,
VaultFilterComponent,
@@ -180,7 +186,7 @@ import "../platform/popup/locales";
ViewCustomFieldsComponent,
RemovePasswordComponent,
VaultSelectComponent,
Fido2Component,
Fido2V1Component,
AutofillV1Component,
EnvironmentSelectorComponent,
],

View File

@@ -217,7 +217,7 @@ app-vault-attachments {
}
}
app-fido2 {
app-fido2-v1 {
.auth-wrapper {
display: flex;
flex-direction: column;

View File

@@ -15,6 +15,7 @@ import {
SectionComponent,
SectionHeaderComponent,
ToastService,
TypographyModule,
} from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -30,6 +31,7 @@ import { PasswordRepromptService } from "@bitwarden/vault";
SectionHeaderComponent,
MenuModule,
IconButtonModule,
TypographyModule,
],
})
export class TrashListItemsContainerComponent {

View File

@@ -5,7 +5,11 @@
</ng-container>
</popup-header>
<ng-container *ngIf="deletedCiphers$ | async as deletedItems">
<bit-callout *ngIf="deletedItems.length" type="warning" title="{{ 'warning' | i18n }}">
<bit-callout
*ngIf="deletedItems.length"
type="warning"
title="{{ 'warning' | i18n | titlecase }}"
>
{{ "trashWarning" | i18n }}
</bit-callout>

View File

@@ -12,7 +12,7 @@
"baseUrl": ".",
"lib": ["ES2021.String"],
"paths": {
"@bitwarden/admin-console": ["../../libs/admin-console/src"],
"@bitwarden/admin-console/common": ["../../libs/admin-console/src/common"],
"@bitwarden/angular/*": ["../../libs/angular/src/*"],
"@bitwarden/auth/common": ["../../libs/auth/src/common"],
"@bitwarden/auth/angular": ["../../libs/auth/src/angular"],

View File

@@ -1,5 +1,5 @@
{
"flags": {
"enableCipherKeyEncryption": true
"enableCipherKeyEncryption": false
}
}

View File

@@ -1,5 +1,5 @@
{
"flags": {
"enableCipherKeyEncryption": true
"enableCipherKeyEncryption": false
}
}

View File

@@ -1,6 +1,8 @@
import {
OrganizationUserApiService,
OrganizationUserConfirmRequest,
} from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserConfirmRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -10,7 +12,7 @@ export class ConfirmCommand {
constructor(
private apiService: ApiService,
private cryptoService: CryptoService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
) {}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
@@ -42,7 +44,7 @@ export class ConfirmCommand {
if (orgKey == null) {
throw new Error("No encryption key for this organization.");
}
const orgUser = await this.organizationUserService.getOrganizationUser(
const orgUser = await this.organizationUserApiService.getOrganizationUser(
options.organizationId,
id,
);
@@ -54,7 +56,7 @@ export class ConfirmCommand {
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey);
const req = new OrganizationUserConfirmRequest();
req.key = key.encryptedString;
await this.organizationUserService.postOrganizationUserConfirm(
await this.organizationUserApiService.postOrganizationUserConfirm(
options.organizationId,
id,
req,

View File

@@ -1,10 +1,10 @@
import { firstValueFrom } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { EventType } from "@bitwarden/common/enums";
import { ListResponse as ApiListResponse } from "@bitwarden/common/models/response/list.response";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -35,7 +35,7 @@ export class ListCommand {
private collectionService: CollectionService,
private organizationService: OrganizationService,
private searchService: SearchService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private apiService: ApiService,
private eventCollectionService: EventCollectionService,
) {}
@@ -211,7 +211,7 @@ export class ListCommand {
}
try {
const response = await this.organizationUserService.getAllUsers(options.organizationId);
const response = await this.organizationUserApiService.getAllUsers(options.organizationId);
const res = new ListResponse(
response.data.map((r) => {
const u = new OrganizationUserResponse();

View File

@@ -71,7 +71,7 @@ export class OssServeConfigurator {
this.serviceContainer.collectionService,
this.serviceContainer.organizationService,
this.serviceContainer.searchService,
this.serviceContainer.organizationUserService,
this.serviceContainer.organizationUserApiService,
this.serviceContainer.apiService,
this.serviceContainer.eventCollectionService,
);
@@ -114,7 +114,7 @@ export class OssServeConfigurator {
this.confirmCommand = new ConfirmCommand(
this.serviceContainer.apiService,
this.serviceContainer.cryptoService,
this.serviceContainer.organizationUserService,
this.serviceContainer.organizationUserApiService,
);
this.restoreCommand = new RestoreCommand(this.serviceContainer.cipherService);
this.shareCommand = new ShareCommand(

View File

@@ -4,6 +4,10 @@ import * as path from "path";
import * as jsdom from "jsdom";
import { firstValueFrom } from "rxjs";
import {
OrganizationUserApiService,
DefaultOrganizationUserApiService,
} from "@bitwarden/admin-console/common";
import {
InternalUserDecryptionOptionsServiceAbstraction,
AuthRequestService,
@@ -16,12 +20,10 @@ import {
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation";
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service";
@@ -184,7 +186,7 @@ export class ServiceContainer {
environmentService: EnvironmentService;
cipherService: CipherService;
folderService: InternalFolderService;
organizationUserService: OrganizationUserService;
organizationUserApiService: OrganizationUserApiService;
collectionService: CollectionService;
vaultTimeoutService: VaultTimeoutService;
masterPasswordService: InternalMasterPasswordServiceAbstraction;
@@ -492,7 +494,7 @@ export class ServiceContainer {
this.providerService = new ProviderService(this.stateProvider);
this.organizationUserService = new OrganizationUserServiceImplementation(this.apiService);
this.organizationUserApiService = new DefaultOrganizationUserApiService(this.apiService);
this.policyApiService = new PolicyApiService(this.policyService, this.apiService);

View File

@@ -108,7 +108,7 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.collectionService,
this.serviceContainer.organizationService,
this.serviceContainer.searchService,
this.serviceContainer.organizationUserService,
this.serviceContainer.organizationUserApiService,
this.serviceContainer.apiService,
this.serviceContainer.eventCollectionService,
);
@@ -412,7 +412,7 @@ export class VaultProgram extends BaseProgram {
const command = new ConfirmCommand(
this.serviceContainer.apiService,
this.serviceContainer.cryptoService,
this.serviceContainer.organizationUserService,
this.serviceContainer.organizationUserApiService,
);
const response = await command.run(object, id, cmd);
this.processResponse(response);

View File

@@ -13,6 +13,7 @@
"baseUrl": ".",
"paths": {
"@bitwarden/common/spec": ["../../libs/common/spec"],
"@bitwarden/admin-console/common": ["../../libs/admin-console/src/common"],
"@bitwarden/auth/common": ["../../libs/auth/src/common"],
"@bitwarden/auth/angular": ["../../libs/auth/src/angular"],
"@bitwarden/common/*": ["../../libs/common/src/*"],

View File

@@ -1,6 +1,6 @@
{
"devFlags": {},
"flags": {
"enableCipherKeyEncryption": true
"enableCipherKeyEncryption": false
}
}

View File

@@ -1,6 +1,6 @@
{
"devFlags": {},
"flags": {
"enableCipherKeyEncryption": true
"enableCipherKeyEncryption": false
}
}

View File

@@ -1,5 +1,5 @@
{
"flags": {
"enableCipherKeyEncryption": true
"enableCipherKeyEncryption": false
}
}

View File

@@ -4,3 +4,4 @@ index.node
**/.DS_Store
npm-debug.log*
*.node
dist

View File

@@ -440,9 +440,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.126"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c4eae4b7fc8dcb0032eb3b1beee46b38d371cdeaf2d0c64b9944f6f69ad7755"
checksum = "54ccead7d199d584d139148b04b4a368d1ec7556a1d9ea2548febb1b9d49f9a4"
dependencies = [
"cc",
"cxxbridge-flags",
@@ -452,9 +452,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.126"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c822bf7fb755d97328d6c337120b6f843678178751cba33c9da25cf522272e0"
checksum = "c77953e99f01508f89f55c494bfa867171ef3a6c8cea03d26975368f2121a5c1"
dependencies = [
"cc",
"codespan-reporting",
@@ -467,21 +467,30 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.126"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719d6197dc016c88744aff3c0d0340a01ecce12e8939fc282e7c8f583ee64bc6"
checksum = "65777e06cc48f0cb0152024c77d6cf9e4bdb4408e7b48bea993d42fa0f5b02b6"
[[package]]
name = "cxxbridge-macro"
version = "1.0.126"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35de3b547387863c8f82013c4f79f1c2162edee956383e4089e1d04c18c4f16c"
checksum = "98532a60dedaebc4848cb2cba5023337cc9ea3af16a5b062633fabfd9f18fb60"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "derive-new"
version = "0.6.0"
@@ -503,10 +512,14 @@ dependencies = [
"base64",
"cbc",
"core-foundation",
"dirs",
"futures",
"gio",
"interprocess",
"keytar",
"libc",
"libsecret",
"log",
"rand",
"retry",
"scopeguard",
@@ -515,6 +528,7 @@ dependencies = [
"sha2",
"thiserror",
"tokio",
"tokio-util",
"typenum",
"widestring",
"windows",
@@ -531,6 +545,21 @@ dependencies = [
"napi",
"napi-build",
"napi-derive",
"tokio",
"tokio-util",
]
[[package]]
name = "desktop_proxy"
version = "0.0.0"
dependencies = [
"anyhow",
"desktop_core",
"futures",
"log",
"simplelog",
"tokio",
"tokio-util",
]
[[package]]
@@ -543,6 +572,27 @@ dependencies = [
"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]]
name = "dlib"
version = "0.5.2"
@@ -552,6 +602,12 @@ dependencies = [
"libloading",
]
[[package]]
name = "doctest-file"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
[[package]]
name = "downcast-rs"
version = "1.2.1"
@@ -646,6 +702,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "futures-channel"
version = "0.3.30"
@@ -653,6 +724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@@ -720,6 +792,7 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@@ -896,9 +969,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [
"equivalent",
"hashbrown",
@@ -914,6 +987,27 @@ dependencies = [
"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]]
name = "keytar"
version = "0.1.6"
@@ -951,6 +1045,16 @@ dependencies = [
"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]]
name = "libsecret"
version = "0.5.0"
@@ -1039,10 +1143,21 @@ dependencies = [
]
[[package]]
name = "napi"
version = "2.16.6"
name = "mio"
version = "0.8.11"
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 = [
"bitflags",
"ctor",
@@ -1060,9 +1175,9 @@ checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a"
[[package]]
name = "napi-derive"
version = "2.16.5"
version = "2.16.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0e034ddf6155192cf83f267ede763fe6c164dfa9971585436b16173718d94c4"
checksum = "70a8a778fd367b13c64232e58632514b795514ece491ce136d96e976d34a3eb8"
dependencies = [
"cfg-if",
"convert_case",
@@ -1131,6 +1246,12 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num_cpus"
version = "1.16.0"
@@ -1141,6 +1262,15 @@ dependencies = [
"libc",
]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "objc-sys"
version = "0.3.5"
@@ -1242,9 +1372,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.36.3"
version = "0.36.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9"
checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a"
dependencies = [
"memchr",
]
@@ -1255,6 +1385,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-stream"
version = "0.2.0"
@@ -1358,6 +1494,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@@ -1433,6 +1575,12 @@ dependencies = [
"getrandom",
]
[[package]]
name = "recvmsg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
[[package]]
name = "redox_syscall"
version = "0.5.3"
@@ -1442,6 +1590,17 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox",
"thiserror",
]
[[package]]
name = "regex"
version = "1.10.6"
@@ -1623,6 +1782,17 @@ dependencies = [
"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]]
name = "slab"
version = "0.4.9"
@@ -1638,6 +1808,16 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "static_assertions"
version = "1.1.0"
@@ -1646,9 +1826,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "2.0.76"
version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [
"proc-macro2",
"quote",
@@ -1716,6 +1896,39 @@ dependencies = [
"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]]
name = "tokio"
version = "1.38.0"
@@ -1724,9 +1937,13 @@ checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"num_cpus",
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys 0.48.0",
]
[[package]]
@@ -1740,6 +1957,19 @@ dependencies = [
"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]]
name = "toml"
version = "0.8.19"
@@ -2035,6 +2265,15 @@ dependencies = [
"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]]
name = "windows-sys"
version = "0.52.0"

View File

@@ -1,3 +1,3 @@
[workspace]
resolver = "2"
members = ["napi", "core"]
members = ["napi", "core", "proxy"]

View 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}`));
});

View File

@@ -6,9 +6,21 @@ version = "0.0.0"
publish = false
[features]
default = []
default = ["sys"]
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]
aes = "=0.8.4"
anyhow = "=1.0.86"
@@ -17,17 +29,22 @@ arboard = { version = "=3.4.0", default-features = false, features = [
] }
base64 = "=0.22.1"
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"
log = "=0.4.22"
rand = "=0.8.5"
retry = "=2.0.0"
scopeguard = "=1.2.0"
sha2 = "=0.10.8"
thiserror = "=1.0.61"
tokio = { version = "=1.38.0", features = ["io-util", "sync", "macros"] }
tokio-util = "=0.7.11"
typenum = "=1.17.0"
[target.'cfg(windows)'.dependencies]
widestring = "=1.1.0"
widestring = { version = "=1.1.0", optional = true }
windows = { version = "=0.57.0", features = [
"Foundation",
"Security_Credentials_UI",
@@ -38,18 +55,18 @@ windows = { version = "=0.57.0", features = [
"Win32_System_WinRT",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
] }
], optional = true }
[target.'cfg(windows)'.dev-dependencies]
keytar = "=0.1.6"
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "=0.9.4"
security-framework = "=2.11.0"
security-framework-sys = "=2.11.0"
core-foundation = { version = "=0.9.4", optional = true }
security-framework = { version = "=2.11.0", optional = true }
security-framework-sys = { version = "=2.11.0", optional = true }
[target.'cfg(target_os = "linux")'.dependencies]
gio = "=0.19.5"
libsecret = "=0.5.0"
zbus = "=4.3.1"
zbus_polkit = "=4.0.0"
gio = { version = "=0.19.5", optional = true }
libsecret = { version = "=0.5.0", optional = true }
zbus = { version = "=4.3.1", optional = true }
zbus_polkit = { version = "=4.0.0", optional = true }

View File

@@ -0,0 +1,102 @@
use std::{
path::{Path, PathBuf},
time::Duration,
};
use interprocess::local_socket::{
tokio::{prelude::*, Stream},
GenericFilePath, ToFsName,
};
use log::{error, info};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
time::sleep,
};
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>,
) {
// Keep track of connection failures to make sure we don't leave the process as a zombie
let mut connection_failures = 0;
loop {
match connect_inner(&path, &send, &mut recv).await {
Ok(()) => return,
Err(e) => {
connection_failures += 1;
if connection_failures >= 20 {
error!("Failed to connect to IPC server after 20 attempts: {e}");
return;
}
error!("Failed to connect to IPC server: {e}");
}
}
sleep(Duration::from_secs(5)).await;
}
}
async fn connect_inner(
path: &Path,
send: &tokio::sync::mpsc::Sender<String>,
recv: &mut 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(())
}

View File

@@ -0,0 +1,64 @@
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();
}
home.join(format!(
"Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop/tmp/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}"))
}
}

View 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(())
}

View File

@@ -1,7 +1,13 @@
#[cfg(feature = "sys")]
pub mod biometric;
#[cfg(feature = "sys")]
pub mod clipboard;
pub mod crypto;
pub mod error;
pub mod ipc;
#[cfg(feature = "sys")]
pub mod password;
#[cfg(feature = "sys")]
pub mod process_isolation;
#[cfg(feature = "sys")]
pub mod powermonitor;

View File

@@ -16,8 +16,10 @@ manual_test = []
[dependencies]
anyhow = "=1.0.86"
desktop_core = { path = "../core" }
napi = { version = "=2.16.6", features = ["async"] }
napi-derive = "=2.16.5"
napi = { version = "=2.16.7", features = ["async"] }
napi-derive = "=2.16.6"
tokio = { version = "1.38.0" }
tokio-util = "0.7.11"
[build-dependencies]
napi-build = "=2.1.3"

View File

@@ -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'});
});

View File

@@ -51,3 +51,33 @@ export namespace powermonitors {
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
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
}
}

View File

@@ -206,10 +206,4 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
const { passwords, biometrics, clipboards, processisolations, powermonitors } = nativeBinding
module.exports.passwords = passwords
module.exports.biometrics = biometrics
module.exports.clipboards = clipboards
module.exports.processisolations = processisolations
module.exports.powermonitors = powermonitors
module.exports = nativeBinding

View File

@@ -3,9 +3,7 @@
"version": "0.1.0",
"description": "",
"scripts": {
"build": "napi build --release --platform --js false",
"build:debug": "napi build --platform --js false",
"build:cross-platform": "node build.js",
"build": "napi build --platform --js false",
"test": "cargo test"
},
"author": "",

View File

@@ -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())
}
}
}

View File

@@ -0,0 +1,16 @@
[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"] }

View File

@@ -0,0 +1,137 @@
use std::path::Path;
use desktop_core::ipc::{MESSAGE_CHANNEL_BUFFER, NATIVE_MESSAGING_BUFFER_SIZE};
use futures::{SinkExt, StreamExt};
use log::*;
use tokio_util::codec::LengthDelimitedCodec;
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,
));
// 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! {
// IPC client has finished, so we should exit as well.
_ = &mut handle => {
break;
}
// 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.");
break;
}
}
},
// 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);
break;
}
None => {
info!("Received EOF, exiting.");
break;
}
}
}
}
}
}

View File

@@ -73,6 +73,13 @@
"CFBundleDevelopmentRegion": "en"
},
"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"]
},
"win": {
@@ -84,16 +91,24 @@
"from": "../../node_modules/regedit/vbs",
"to": "regedit/vbs",
"filter": ["**/*"]
},
}
],
"extraFiles": [
{
"from": "resources/native-messaging.bat",
"to": "native-messaging.bat"
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe",
"to": "desktop_proxy.exe"
}
]
},
"linux": {
"category": "Utility",
"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"],
"desktop": {
"Name": "Bitwarden",

View File

@@ -18,7 +18,7 @@
"yargs": "17.7.2"
},
"devDependencies": {
"@types/node": "20.16.1",
"@types/node": "20.16.4",
"@types/node-ipc": "9.2.3",
"typescript": "4.7.4"
}
@@ -106,9 +106,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.16.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz",
"integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==",
"version": "20.16.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.4.tgz",
"integrity": "sha512-ioyQ1zK9aGEomJ45zz8S8IdzElyxhvP1RVWnPrXDf6wFaUb+kk1tEcVVJkF7RPGM0VWI7cp5U57oCPIn5iN1qg==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
@@ -241,9 +241,9 @@
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"

View File

@@ -23,7 +23,7 @@
"yargs": "17.7.2"
},
"devDependencies": {
"@types/node": "20.16.1",
"@types/node": "20.16.4",
"@types/node-ipc": "9.2.3",
"typescript": "4.7.4"
},

View File

@@ -18,7 +18,7 @@
"scripts": {
"postinstall": "electron-rebuild",
"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: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",

View File

@@ -0,0 +1,14 @@
<?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.application-identifier</key>
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
<key>com.apple.developer.team-identifier</key>
<string>LTZ2PFU5D6</string>
<key>com.apple.security.application-groups</key>
<array>
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
</array>
</dict>
</plist>

View File

@@ -8,6 +8,10 @@
<string>LTZ2PFU5D6</string>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>

View File

@@ -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 %*

View File

@@ -1,14 +1,22 @@
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
require("dotenv").config();
const child_process = require("child_process");
const path = require("path");
const { flipFuses, FuseVersion, FuseV1Options } = require("@electron/fuses");
const builder = require("electron-builder");
const fse = require("fs-extra");
exports.default = run;
async function run(context) {
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") {
console.log("Creating memory-protection wrapper script");
const appOutDir = context.appOutDir;
@@ -23,4 +31,114 @@ async function run(context) {
fse.chmodSync(wrapperBin, "755");
console.log("Copied memory-protection wrapper script");
}
if (["darwin", "mas"].includes(context.electronPlatformName)) {
const identities = getIdentities(process.env.CSC_NAME ?? "");
if (identities.length === 0) {
throw new Error("No valid identities found");
}
const 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(csc_name) {
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;
})
.filter((line) => line.includes(csc_name))
.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,
});
}

View File

@@ -1,6 +1,6 @@
import { animate, state, style, transition, trigger } from "@angular/animations";
import { ConnectedPosition } from "@angular/cdk/overlay";
import { Component } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
@@ -51,7 +51,7 @@ type InactiveAccount = ActiveAccount & {
]),
],
})
export class AccountSwitcherComponent {
export class AccountSwitcherComponent implements OnInit {
activeAccount$: Observable<ActiveAccount | null>;
inactiveAccounts$: Observable<{ [userId: string]: InactiveAccount }>;
authStatus = AuthenticationStatus;
@@ -151,6 +151,24 @@ export class AccountSwitcherComponent {
);
}
async ngOnInit() {
const active = await firstValueFrom(this.accountService.activeAccount$);
if (active == null) {
return;
}
const authStatus = await firstValueFrom(
this.authService.authStatuses$.pipe(map((statuses) => statuses[active.id])),
);
if (authStatus === AuthenticationStatus.LoggedOut) {
const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$);
if (nextUpAccount != null) {
await this.switch(nextUpAccount.id);
} else {
await this.addAccount();
}
}
}
toggle() {
this.isOpen = !this.isOpen;
}

View File

@@ -1,6 +1,7 @@
import { APP_INITIALIZER, NgModule } from "@angular/core";
import { Subject, merge } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import {
SECURE_STORAGE,
@@ -26,7 +27,6 @@ import {
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
@@ -285,7 +285,7 @@ const safeProviders: SafeProvider[] = [
KdfConfigService,
InternalMasterPasswordServiceAbstraction,
OrganizationApiServiceAbstraction,
OrganizationUserService,
OrganizationUserApiService,
InternalUserDecryptionOptionsServiceAbstraction,
],
}),

View File

@@ -1,11 +1,11 @@
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -50,7 +50,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
private ngZone: NgZone,
stateService: StateService,
organizationApiService: OrganizationApiServiceAbstraction,
organizationUserService: OrganizationUserService,
organizationUserApiService: OrganizationUserApiService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService,
@@ -74,7 +74,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
route,
stateService,
organizationApiService,
organizationUserService,
organizationUserApiService,
userDecryptionOptionsService,
ssoLoginService,
dialogService,

View File

@@ -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
// 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.
import { app } from "electron";
if (
process.platform === "darwin" &&
process.argv.some((arg) => arg.indexOf("chrome-extension://") !== -1 || arg.indexOf("{") !== -1)
) {
if (process.platform === "darwin") {
// eslint-disable-next-line
const app = require("electron").app;
// If we're on MacOS, we need to support DuckDuckGo's IPC communication,
// which for the moment is launching the Bitwarden process.
// 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.dock.hide();
});
}
process.stdout.on("error", (e) => {
if (e.code === "EPIPE") {
process.exit(0);
}
app.on("ready", () => {
app.dock.hide();
});
const proxy = new NativeMessagingProxy();
proxy.run();
const proc = spawn(path.join(process.execPath, "..", "desktop_proxy"), process.argv.slice(1), {
cwd: process.cwd(),
stdio: "inherit",
shell: false,
});
proc.on("exit", () => {
process.exit(0);
});
proc.on("error", () => {
process.exit(1);
});
} else {
// eslint-disable-next-line
const Main = require("./main").Main;

View File

@@ -220,6 +220,7 @@ export class Main {
this.windowMain,
app.getPath("userData"),
app.getPath("exe"),
app.getAppPath(),
);
this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider);
@@ -265,13 +266,21 @@ export class Main {
if (browserIntegrationEnabled || ddgIntegrationEnabled) {
// Re-register the native messaging host integrations on startup, in case they are not present
if (browserIntegrationEnabled) {
this.nativeMessagingMain.generateManifests().catch(this.logService.error);
this.nativeMessagingMain
.generateManifests()
.catch((err) => this.logService.error("Error while generating manifests", err));
}
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");

View File

@@ -1,34 +1,34 @@
import { existsSync, promises as fs } from "fs";
import { Socket } from "net";
import { homedir, userInfo } from "os";
import * as path from "path";
import * as util from "util";
import { ipcMain } from "electron";
import * as ipc from "node-ipc";
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";
export class NativeMessagingMain {
private connected: Socket[] = [];
private socket: any;
private ipcServer: ipc.IpcServer | null;
private connected: number[] = [];
constructor(
private logService: LogService,
private windowMain: WindowMain,
private userPath: string,
private exePath: string,
private appPath: string,
) {
ipcMain.handle(
"nativeMessaging.manifests",
async (_event: any, options: { create: boolean }) => {
if (options.create) {
this.listen();
try {
await this.listen();
await this.generateManifests();
} catch (e) {
this.logService.error("Error generating manifests: " + e);
@@ -51,8 +51,8 @@ export class NativeMessagingMain {
"nativeMessaging.ddgManifests",
async (_event: any, options: { create: boolean }) => {
if (options.create) {
this.listen();
try {
await this.listen();
await this.generateDdgManifests();
} catch (e) {
this.logService.error("Error generating duckduckgo manifests: " + e);
@@ -72,56 +72,46 @@ export class NativeMessagingMain {
);
}
listen() {
ipc.config.id = "bitwarden";
ipc.config.retry = 1500;
const ipcSocketRoot = getIpcSocketRoot();
if (ipcSocketRoot != null) {
ipc.config.socketRoot = ipcSocketRoot;
async listen() {
if (this.ipcServer) {
this.ipcServer.stop();
}
ipc.serve(() => {
ipc.server.on("message", (data: any, socket: any) => {
this.socket = socket;
this.windowMain.win.webContents.send("nativeMessaging", data);
});
ipcMain.on("nativeMessagingReply", (event, msg) => {
if (this.socket != null && msg != null) {
this.send(msg, this.socket);
this.ipcServer = await ipc.IpcServer.listen("bitwarden", (error, msg) => {
switch (msg.kind) {
case ipc.IpcMessageType.Connected: {
this.connected.push(msg.clientId);
this.logService.info("Native messaging client " + msg.clientId + " has connected");
break;
}
});
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.connected.push(socket);
});
ipc.server.on("socket.disconnected", (socket, destroyedSocketID) => {
const index = this.connected.indexOf(socket);
if (index > -1) {
this.connected.splice(index, 1);
this.logService.info("Native messaging client " + msg.clientId + " has disconnected");
break;
}
this.socket = null;
ipc.log("client " + destroyedSocketID + " has disconnected!");
});
case ipc.IpcMessageType.Message:
this.windowMain.win.webContents.send("nativeMessaging", JSON.parse(msg.message));
break;
}
});
ipc.server.start();
}
stop() {
ipc.server.stop();
// Kill all existing connections
this.connected.forEach((socket) => {
if (!socket.destroyed) {
socket.destroy();
ipcMain.on("nativeMessagingReply", (event, msg) => {
if (msg != null) {
this.send(msg);
}
});
}
send(message: object, socket: any) {
ipc.server.emit(socket, "message", message);
stop() {
this.ipcServer?.stop();
}
send(message: object) {
this.ipcServer?.send(JSON.stringify(message));
}
async generateManifests() {
@@ -331,11 +321,20 @@ export class NativeMessagingMain {
}
private binaryPath() {
if (process.platform === "win32") {
return path.join(path.dirname(this.exePath), "resources", "native-messaging.bat");
const ext = process.platform === "win32" ? ".exe" : "";
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() {

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
});
}
}

View File

@@ -10,7 +10,7 @@
"types": [],
"baseUrl": ".",
"paths": {
"@bitwarden/admin-console": ["../../libs/admin-console/src"],
"@bitwarden/admin-console/common": ["../../libs/admin-console/src/common"],
"@bitwarden/angular/*": ["../../libs/angular/src/*"],
"@bitwarden/auth/common": ["../../libs/auth/src/common"],
"@bitwarden/auth/angular": ["../../libs/auth/src/angular"],

View File

@@ -12,6 +12,6 @@
},
"flags": {
"showPasswordless": false,
"enableCipherKeyEncryption": true
"enableCipherKeyEncryption": false
}
}

View File

@@ -18,6 +18,6 @@
},
"flags": {
"showPasswordless": true,
"enableCipherKeyEncryption": true
"enableCipherKeyEncryption": false
}
}

View File

@@ -21,7 +21,7 @@
],
"flags": {
"showPasswordless": true,
"enableCipherKeyEncryption": true
"enableCipherKeyEncryption": false
},
"devFlags": {}
}

View File

@@ -12,6 +12,6 @@
},
"flags": {
"showPasswordless": true,
"enableCipherKeyEncryption": true
"enableCipherKeyEncryption": false
}
}

View File

@@ -28,6 +28,6 @@
],
"flags": {
"showPasswordless": true,
"enableCipherKeyEncryption": true
"enableCipherKeyEncryption": false
}
}

View File

@@ -8,6 +8,6 @@
},
"flags": {
"showPasswordless": true,
"enableCipherKeyEncryption": true
"enableCipherKeyEncryption": false
}
}

View File

@@ -1,11 +1,11 @@
import { Injectable } from "@angular/core";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import {
OrganizationUserApiService,
OrganizationUserInviteRequest,
OrganizationUserUpdateRequest,
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { OrganizationUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
OrganizationUserDetailsResponse,
} from "@bitwarden/admin-console/common";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CoreOrganizationModule } from "../core-organization.module";
@@ -15,14 +15,14 @@ import { OrganizationUserAdminView } from "../views/organization-user-admin-view
export class UserAdminService {
constructor(
private configService: ConfigService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
) {}
async get(
organizationId: string,
organizationUserId: string,
): Promise<OrganizationUserAdminView | undefined> {
const userResponse = await this.organizationUserService.getOrganizationUser(
const userResponse = await this.organizationUserApiService.getOrganizationUser(
organizationId,
organizationUserId,
{
@@ -47,7 +47,11 @@ export class UserAdminService {
request.groups = user.groups;
request.accessSecretsManager = user.accessSecretsManager;
await this.organizationUserService.putOrganizationUser(user.organizationId, user.id, request);
await this.organizationUserApiService.putOrganizationUser(
user.organizationId,
user.id,
request,
);
}
async invite(emails: string[], user: OrganizationUserAdminView): Promise<void> {
@@ -59,7 +63,7 @@ export class UserAdminService {
request.groups = user.groups;
request.accessSecretsManager = user.accessSecretsManager;
await this.organizationUserService.postOrganizationUserInvite(user.organizationId, request);
await this.organizationUserApiService.postOrganizationUserInvite(user.organizationId, request);
}
private async decryptMany(

View File

@@ -1,4 +1,4 @@
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common";
import {
OrganizationUserStatusType,
OrganizationUserType,

View File

@@ -6,31 +6,27 @@
<div bitDialogContent>
<form [formGroup]="filterFormGroup" [bitSubmit]="refreshEvents">
<div class="tw-flex tw-items-center tw-space-x-2">
<div>
<label class="tw-sr-only" for="start">{{ "startDate" | i18n }}</label>
<span>
<input
bitInput
type="datetime-local"
id="start"
placeholder="{{ 'startDate' | i18n }}"
formControlName="start"
/>
</span>
</div>
<bit-form-field>
<bit-label>{{ "from" | i18n }}</bit-label>
<input
bitInput
type="datetime-local"
id="start"
placeholder="{{ 'startDate' | i18n }}"
formControlName="start"
/>
</bit-form-field>
<span class="tw-mx-2">-</span>
<div>
<label class="tw-sr-only" for="end">{{ "endDate" | i18n }}</label>
<span>
<input
bitInput
type="datetime-local"
id="end"
placeholder="{{ 'endDate' | i18n }}"
formControlName="end"
/>
</span>
</div>
<bit-form-field>
<bit-label>{{ "to" | i18n }}</bit-label>
<input
bitInput
type="datetime-local"
id="end"
placeholder="{{ 'endDate' | i18n }}"
formControlName="end"
/>
</bit-form-field>
<button type="submit" bitButton buttonType="primary" bitFormButton>
<i class="bwi bwi-refresh bwi-fw" aria-hidden="true"></i>
{{ "refresh" | i18n }}

View File

@@ -2,9 +2,9 @@ import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { EventResponse } from "@bitwarden/common/models/response/event.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { EventView } from "@bitwarden/common/models/view/event.view";
@@ -60,7 +60,7 @@ export class EntityEventsComponent implements OnInit {
private platformUtilsService: PlatformUtilsService,
private userNamePipe: UserNamePipe,
private logService: LogService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private formBuilder: FormBuilder,
private validationService: ValidationService,
private toastService: ToastService,
@@ -78,7 +78,9 @@ export class EntityEventsComponent implements OnInit {
async load() {
try {
if (this.showUser) {
const response = await this.organizationUserService.getAllUsers(this.params.organizationId);
const response = await this.organizationUserApiService.getAllUsers(
this.params.organizationId,
);
response.data.forEach((u) => {
const name = this.userNamePipe.transform(u);
this.orgUsersIdMap.set(u.id, { name: name, email: u.email });

View File

@@ -2,10 +2,10 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { concatMap, Subject, takeUntil } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { EventSystemUser } from "@bitwarden/common/enums";
@@ -49,7 +49,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
logService: LogService,
private userNamePipe: UserNamePipe,
private organizationService: OrganizationService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private providerService: ProviderService,
fileDownloadService: FileDownloadService,
toastService: ToastService,
@@ -83,7 +83,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
}
async load() {
const response = await this.organizationUserService.getAllUsers(this.organizationId);
const response = await this.organizationUserApiService.getAllUsers(this.organizationId);
response.data.forEach((u) => {
const name = this.userNamePipe.transform(u);
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });

View File

@@ -14,9 +14,9 @@ import {
takeUntil,
} from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
@@ -131,7 +131,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
);
private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> {
return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe(
return from(this.organizationUserApiService.getAllUsers(this.organizationId)).pipe(
map((response) =>
response.data.map((m) => ({
id: m.id,
@@ -202,7 +202,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
private dialogRef: DialogRef<GroupAddEditDialogResultType>,
private apiService: ApiService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private groupService: GroupService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,

View File

@@ -3,7 +3,7 @@ import { Directive, OnInit } from "@angular/core";
import {
OrganizationUserBulkPublicKeyResponse,
OrganizationUserBulkResponse,
} from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
} from "@bitwarden/admin-console/common";
import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response";
import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";

View File

@@ -1,6 +1,6 @@
import { Directive } from "@angular/core";
import { OrganizationUserBulkResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { OrganizationUserBulkResponse } from "@bitwarden/admin-console/common";
import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -1,9 +1,11 @@
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import {
OrganizationUserApiService,
OrganizationUserBulkConfirmRequest,
} from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserBulkConfirmRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -40,7 +42,7 @@ export class BulkConfirmComponent implements OnInit {
@Inject(DIALOG_DATA) protected data: BulkConfirmDialogData,
protected cryptoService: CryptoService,
protected apiService: ApiService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private i18nService: I18nService,
) {
this.organizationId = data.organizationId;
@@ -104,7 +106,7 @@ export class BulkConfirmComponent implements OnInit {
}
protected async getPublicKeys() {
return await this.organizationUserService.postOrganizationUsersPublicKey(
return await this.organizationUserApiService.postOrganizationUsersPublicKey(
this.organizationId,
this.filteredUsers.map((user) => user.id),
);
@@ -116,7 +118,7 @@ export class BulkConfirmComponent implements OnInit {
protected async postConfirmRequest(userIdsWithKeys: any[]) {
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
return await this.organizationUserService.postOrganizationUserBulkConfirm(
return await this.organizationUserApiService.postOrganizationUserBulkConfirm(
this.organizationId,
request,
);

View File

@@ -1,7 +1,7 @@
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, TableDataSource, ToastService } from "@bitwarden/components";
@@ -21,7 +21,7 @@ export class BulkEnableSecretsManagerDialogComponent implements OnInit {
constructor(
public dialogRef: DialogRef,
@Inject(DIALOG_DATA) private data: BulkEnableSecretsManagerDialogData,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private toastService: ToastService,
@@ -32,7 +32,7 @@ export class BulkEnableSecretsManagerDialogComponent implements OnInit {
}
submit = async () => {
await this.organizationUserService.putOrganizationUserBulkEnableSecretsManager(
await this.organizationUserApiService.putOrganizationUserBulkEnableSecretsManager(
this.data.orgId,
this.dataSource.data.map((u) => u.id),
);

View File

@@ -1,8 +1,8 @@
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "@bitwarden/components";
@@ -33,7 +33,7 @@ export class BulkRemoveComponent {
@Inject(DIALOG_DATA) protected data: BulkRemoveDialogData,
protected apiService: ApiService,
protected i18nService: I18nService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
) {
this.organizationId = data.organizationId;
this.users = data.users;
@@ -60,7 +60,7 @@ export class BulkRemoveComponent {
};
protected async removeUsers() {
return await this.organizationUserService.removeManyOrganizationUsers(
return await this.organizationUserApiService.removeManyOrganizationUsers(
this.organizationId,
this.users.map((user) => user.id),
);

View File

@@ -1,7 +1,7 @@
import { DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "@bitwarden/components";
@@ -32,7 +32,7 @@ export class BulkRestoreRevokeComponent {
constructor(
protected i18nService: I18nService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
@Inject(DIALOG_DATA) protected data: BulkRestoreDialogParams,
) {
this.isRevoking = data.isRevoking;
@@ -66,12 +66,12 @@ export class BulkRestoreRevokeComponent {
protected async performBulkUserAction() {
const userIds = this.users.map((user) => user.id);
if (this.isRevoking) {
return await this.organizationUserService.revokeManyOrganizationUsers(
return await this.organizationUserApiService.revokeManyOrganizationUsers(
this.organizationId,
userIds,
);
} else {
return await this.organizationUserService.restoreManyOrganizationUsers(
return await this.organizationUserApiService.restoreManyOrganizationUsers(
this.organizationId,
userIds,
);

View File

@@ -1,7 +1,7 @@
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { OrganizationUserBulkResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { OrganizationUserBulkResponse } from "@bitwarden/admin-console/common";
import {
OrganizationUserStatusType,
ProviderUserStatusType,

View File

@@ -13,8 +13,8 @@ import {
takeUntil,
} from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import {
OrganizationUserStatusType,
OrganizationUserType,
@@ -139,7 +139,7 @@ export class MemberDialogComponent implements OnDestroy {
private collectionAdminService: CollectionAdminService,
private groupService: GroupService,
private userService: UserAdminService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private dialogService: DialogService,
private accountService: AccountService,
organizationService: OrganizationService,
@@ -491,7 +491,7 @@ export class MemberDialogComponent implements OnDestroy {
}
}
await this.organizationUserService.removeOrganizationUser(
await this.organizationUserApiService.removeOrganizationUser(
this.params.organizationId,
this.params.organizationUserId,
);
@@ -528,7 +528,7 @@ export class MemberDialogComponent implements OnDestroy {
}
}
await this.organizationUserService.revokeOrganizationUser(
await this.organizationUserApiService.revokeOrganizationUser(
this.params.organizationId,
this.params.organizationUserId,
);
@@ -547,7 +547,7 @@ export class MemberDialogComponent implements OnDestroy {
return;
}
await this.organizationUserService.restoreOrganizationUser(
await this.organizationUserApiService.restoreOrganizationUser(
this.params.organizationId,
this.params.organizationUserId,
);

View File

@@ -13,15 +13,17 @@ import {
switchMap,
} from "rxjs";
import {
OrganizationUserApiService,
OrganizationUserConfirmRequest,
OrganizationUserUserDetailsResponse,
} from "@bitwarden/admin-console/common";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserConfirmRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { PolicyApiServiceAbstraction as PolicyApiService } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import {
@@ -116,7 +118,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
private syncService: SyncService,
private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private router: Router,
private groupService: GroupService,
private collectionService: CollectionService,
@@ -213,7 +215,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
let collectionsPromise: Promise<Map<string, string>>;
// We don't need both groups and collections for the table, so only load one
const userPromise = this.organizationUserService.getAllUsers(this.organization.id, {
const userPromise = this.organizationUserApiService.getAllUsers(this.organization.id, {
includeGroups: this.organization.useGroups,
includeCollections: !this.organization.useGroups,
});
@@ -270,19 +272,19 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
}
removeUser(id: string): Promise<void> {
return this.organizationUserService.removeOrganizationUser(this.organization.id, id);
return this.organizationUserApiService.removeOrganizationUser(this.organization.id, id);
}
revokeUser(id: string): Promise<void> {
return this.organizationUserService.revokeOrganizationUser(this.organization.id, id);
return this.organizationUserApiService.revokeOrganizationUser(this.organization.id, id);
}
restoreUser(id: string): Promise<void> {
return this.organizationUserService.restoreOrganizationUser(this.organization.id, id);
return this.organizationUserApiService.restoreOrganizationUser(this.organization.id, id);
}
reinviteUser(id: string): Promise<void> {
return this.organizationUserService.postOrganizationUserReinvite(this.organization.id, id);
return this.organizationUserApiService.postOrganizationUserReinvite(this.organization.id, id);
}
async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise<void> {
@@ -290,7 +292,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey);
const request = new OrganizationUserConfirmRequest();
request.key = key.encryptedString;
await this.organizationUserService.postOrganizationUserConfirm(
await this.organizationUserApiService.postOrganizationUserConfirm(
this.organization.id,
user.id,
request,
@@ -585,7 +587,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
}
try {
const response = this.organizationUserService.postManyOrganizationUserReinvite(
const response = this.organizationUserApiService.postManyOrganizationUserReinvite(
this.organization.id,
filteredUsers.map((user) => user.id),
);

View File

@@ -1,8 +1,10 @@
import { mock, MockProxy } from "jest-mock-extended";
import {
OrganizationUserApiService,
OrganizationUserResetPasswordDetailsResponse,
} from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
@@ -24,7 +26,7 @@ describe("OrganizationUserResetPasswordService", () => {
let cryptoService: MockProxy<CryptoService>;
let encryptService: MockProxy<EncryptService>;
let organizationService: MockProxy<OrganizationService>;
let organizationUserService: MockProxy<OrganizationUserService>;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let organizationApiService: MockProxy<OrganizationApiService>;
let i18nService: MockProxy<I18nService>;
@@ -32,7 +34,7 @@ describe("OrganizationUserResetPasswordService", () => {
cryptoService = mock<CryptoService>();
encryptService = mock<EncryptService>();
organizationService = mock<OrganizationService>();
organizationUserService = mock<OrganizationUserService>();
organizationUserApiService = mock<OrganizationUserApiService>();
organizationApiService = mock<OrganizationApiService>();
i18nService = mock<I18nService>();
@@ -40,7 +42,7 @@ describe("OrganizationUserResetPasswordService", () => {
cryptoService,
encryptService,
organizationService,
organizationUserService,
organizationUserApiService,
organizationApiService,
i18nService,
);
@@ -112,7 +114,7 @@ describe("OrganizationUserResetPasswordService", () => {
const mockOrgId = "test-org-id";
beforeEach(() => {
organizationUserService.getOrganizationUserResetPasswordDetails.mockResolvedValue(
organizationUserApiService.getOrganizationUserResetPasswordDetails.mockResolvedValue(
new OrganizationUserResetPasswordDetailsResponse({
kdf: KdfType.PBKDF2_SHA256,
kdfIterations: 5000,
@@ -140,11 +142,11 @@ describe("OrganizationUserResetPasswordService", () => {
it("should reset the user's master password", async () => {
await sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId);
expect(organizationUserService.putOrganizationUserResetPassword).toHaveBeenCalled();
expect(organizationUserApiService.putOrganizationUserResetPassword).toHaveBeenCalled();
});
it("should throw an error if the user details are null", async () => {
organizationUserService.getOrganizationUserResetPasswordDetails.mockResolvedValue(null);
organizationUserApiService.getOrganizationUserResetPasswordDetails.mockResolvedValue(null);
await expect(
sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId),
).rejects.toThrow();

View File

@@ -1,13 +1,13 @@
import { Injectable } from "@angular/core";
import {
OrganizationUserApiService,
OrganizationUserResetPasswordRequest,
OrganizationUserResetPasswordWithIdRequest,
} from "@bitwarden/admin-console/common";
import { UserKeyRotationDataProvider } from "@bitwarden/auth/common";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import {
OrganizationUserResetPasswordRequest,
OrganizationUserResetPasswordWithIdRequest,
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import {
Argon2KdfConfig,
KdfConfig,
@@ -33,7 +33,7 @@ export class OrganizationUserResetPasswordService
private cryptoService: CryptoService,
private encryptService: EncryptService,
private organizationService: OrganizationService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private organizationApiService: OrganizationApiServiceAbstraction,
private i18nService: I18nService,
) {}
@@ -76,7 +76,7 @@ export class OrganizationUserResetPasswordService
orgUserId: string,
orgId: string,
): Promise<void> {
const response = await this.organizationUserService.getOrganizationUserResetPasswordDetails(
const response = await this.organizationUserApiService.getOrganizationUserResetPasswordDetails(
orgId,
orgUserId,
);
@@ -128,7 +128,11 @@ export class OrganizationUserResetPasswordService
request.newMasterPasswordHash = newMasterKeyHash;
// Change user's password
await this.organizationUserService.putOrganizationUserResetPassword(orgId, orgUserId, request);
await this.organizationUserApiService.putOrganizationUserResetPassword(
orgId,
orgUserId,
request,
);
}
/**

View File

@@ -0,0 +1,74 @@
import { Meta, StoryObj } from "@storybook/angular";
import { AccessSelectorComponent, PermissionMode } from "./access-selector.component";
import { AccessItemType, AccessItemValue } from "./access-selector.models";
import { default as baseComponentDefinition } from "./access-selector.stories";
import { actionsData, itemsFactory } from "./storybook-utils";
/**
* Displays the Access Selector in a dialog.
*/
export default {
title: "Web/Organizations/Access Selector/Dialog",
decorators: baseComponentDefinition.decorators,
} as Meta;
type Story = StoryObj<AccessSelectorComponent & { initialValue: AccessItemValue[] }>;
const render: Story["render"] = (args) => ({
props: {
items: [],
valueChanged: actionsData.onValueChanged,
initialValue: [],
...args,
},
template: `
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
<span bitDialogTitle>Access selector</span>
<span bitDialogContent>
<bit-access-selector
(ngModelChange)="valueChanged($event)"
[ngModel]="initialValue"
[items]="items"
[disabled]="disabled"
[columnHeader]="columnHeader"
[showGroupColumn]="showGroupColumn"
[selectorLabelText]="selectorLabelText"
[selectorHelpText]="selectorHelpText"
[emptySelectionText]="emptySelectionText"
[permissionMode]="permissionMode"
[showMemberRoles]="showMemberRoles"
></bit-access-selector>
</span>
<ng-container bitDialogFooter>
<button bitButton buttonType="primary">Save</button>
<button bitButton buttonType="secondary">Cancel</button>
<button
class="tw-ml-auto"
bitIconButton="bwi-trash"
buttonType="danger"
size="default"
title="Delete"
aria-label="Delete"></button>
</ng-container>
</bit-dialog>
`,
});
const dialogAccessItems = itemsFactory(10, AccessItemType.Collection);
export const Dialog: Story = {
args: {
permissionMode: PermissionMode.Edit,
showMemberRoles: false,
showGroupColumn: true,
columnHeader: "Collection",
selectorLabelText: "Select Collections",
selectorHelpText: "Some helper text describing what this does",
emptySelectionText: "No collections added",
disabled: false,
initialValue: [] as any[],
items: dialogAccessItems,
},
render,
};

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