mirror of
https://github.com/bitwarden/browser
synced 2026-02-05 03:03:26 +00:00
Merge branch 'main' into autofill/pm-17641/fix-ssh-agent-default-socket-path
This commit is contained in:
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -29,6 +29,7 @@ libs/common/src/auth @bitwarden/team-auth-dev
|
||||
apps/browser/src/tools @bitwarden/team-tools-dev
|
||||
apps/cli/src/tools @bitwarden/team-tools-dev
|
||||
apps/desktop/src/app/tools @bitwarden/team-tools-dev
|
||||
apps/desktop/desktop_native/bitwarden_chromium_importer @bitwarden/team-tools-dev
|
||||
apps/web/src/app/tools @bitwarden/team-tools-dev
|
||||
libs/angular/src/tools @bitwarden/team-tools-dev
|
||||
libs/common/src/models/export @bitwarden/team-tools-dev
|
||||
@@ -215,3 +216,4 @@ apps/web/src/locales/en/messages.json
|
||||
**/tsconfig.json @bitwarden/team-platform-dev
|
||||
**/jest.config.js @bitwarden/team-platform-dev
|
||||
**/project.jsons @bitwarden/team-platform-dev
|
||||
libs/pricing @bitwarden/team-billing-dev
|
||||
|
||||
79
.github/workflows/build-browser.yml
vendored
79
.github/workflows/build-browser.yml
vendored
@@ -123,11 +123,20 @@ jobs:
|
||||
|
||||
|
||||
build-source:
|
||||
name: Build browser source
|
||||
name: Build browser source - ${{matrix.license_type.readable}}
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- setup
|
||||
- locales-test
|
||||
strategy:
|
||||
matrix:
|
||||
license_type:
|
||||
- include_bitwarden_license_folder: false
|
||||
archive_name_prefix: ""
|
||||
readable: "open source license"
|
||||
- include_bitwarden_license_folder: true
|
||||
archive_name_prefix: "bit-"
|
||||
readable: "commercial license"
|
||||
env:
|
||||
_BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
@@ -166,6 +175,12 @@ jobs:
|
||||
mkdir -p browser-source/apps/browser
|
||||
cp -r apps/browser/* browser-source/apps/browser
|
||||
|
||||
# Copy bitwarden_license/bit-browser to the Browser source directory
|
||||
if [[ ${{matrix.license_type.include_bitwarden_license_folder}} == "true" ]]; then
|
||||
mkdir -p browser-source/bitwarden_license/bit-browser
|
||||
cp -r bitwarden_license/bit-browser/* browser-source/bitwarden_license/bit-browser
|
||||
fi
|
||||
|
||||
# Copy libs to Browser source directory
|
||||
mkdir browser-source/libs
|
||||
cp -r libs/* browser-source/libs
|
||||
@@ -175,13 +190,13 @@ jobs:
|
||||
- name: Upload browser source
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: browser-source-${{ env._BUILD_NUMBER }}.zip
|
||||
name: ${{matrix.license_type.archive_name_prefix}}browser-source-${{ env._BUILD_NUMBER }}.zip
|
||||
path: browser-source.zip
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
build:
|
||||
name: Build
|
||||
name: Build ${{ matrix.browser.name }} - ${{ matrix.license_type.readable }}
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- setup
|
||||
@@ -192,25 +207,38 @@ jobs:
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
license_type:
|
||||
- build_prefix: ""
|
||||
artifact_prefix: ""
|
||||
source_archive_name_prefix: ""
|
||||
archive_name_prefix: ""
|
||||
npm_command_prefix: "dist:"
|
||||
readable: "open source license"
|
||||
- build_prefix: "bit-"
|
||||
artifact_prefix: "bit-"
|
||||
source_archive_name_prefix: "bit-"
|
||||
archive_name_prefix: "bit-"
|
||||
npm_command_prefix: "dist:bit:"
|
||||
readable: "commercial license"
|
||||
browser:
|
||||
- name: "chrome"
|
||||
npm_command: "dist:chrome"
|
||||
npm_command_suffix: "chrome"
|
||||
archive_name: "dist-chrome.zip"
|
||||
artifact_name: "dist-chrome-MV3"
|
||||
- name: "edge"
|
||||
npm_command: "dist:edge"
|
||||
npm_command_suffix: "edge"
|
||||
archive_name: "dist-edge.zip"
|
||||
artifact_name: "dist-edge-MV3"
|
||||
- name: "firefox"
|
||||
npm_command: "dist:firefox"
|
||||
npm_command_suffix: "firefox"
|
||||
archive_name: "dist-firefox.zip"
|
||||
artifact_name: "dist-firefox"
|
||||
- name: "firefox-mv3"
|
||||
npm_command: "dist:firefox:mv3"
|
||||
npm_command_suffix: "firefox:mv3"
|
||||
archive_name: "dist-firefox.zip"
|
||||
artifact_name: "DO-NOT-USE-FOR-PROD-dist-firefox-MV3"
|
||||
- name: "opera-mv3"
|
||||
npm_command: "dist:opera:mv3"
|
||||
npm_command_suffix: "opera:mv3"
|
||||
archive_name: "dist-opera.zip"
|
||||
artifact_name: "dist-opera-MV3"
|
||||
steps:
|
||||
@@ -234,7 +262,7 @@ jobs:
|
||||
- name: Download browser source
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: browser-source-${{ env._BUILD_NUMBER }}.zip
|
||||
name: ${{matrix.license_type.source_archive_name_prefix}}browser-source-${{ env._BUILD_NUMBER }}.zip
|
||||
|
||||
- name: Unzip browser source artifact
|
||||
run: |
|
||||
@@ -264,7 +292,7 @@ jobs:
|
||||
run: npm link ../sdk-internal
|
||||
|
||||
- name: Check source file size
|
||||
if: ${{ startsWith(matrix.name, 'firefox') }}
|
||||
if: ${{ startsWith(matrix.browser.name, 'firefox') }}
|
||||
run: |
|
||||
# Declare variable as indexed array
|
||||
declare -a FILES
|
||||
@@ -287,19 +315,19 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build extension
|
||||
run: npm run ${{ matrix.npm_command }}
|
||||
run: npm run ${{matrix.license_type.npm_command_prefix}}${{ matrix.browser.npm_command_suffix }}
|
||||
working-directory: browser-source/apps/browser
|
||||
|
||||
- name: Upload extension artifact
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}-${{ env._BUILD_NUMBER }}.zip
|
||||
path: browser-source/apps/browser/dist/${{ matrix.archive_name }}
|
||||
name: ${{ matrix.license_type.artifact_prefix }}${{ matrix.browser.artifact_name }}-${{ env._BUILD_NUMBER }}.zip
|
||||
path: browser-source/apps/browser/dist/${{matrix.license_type.archive_name_prefix}}${{ matrix.browser.archive_name }}
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
build-safari:
|
||||
name: Build Safari
|
||||
name: Build Safari - ${{ matrix.license_type.readable }}
|
||||
runs-on: macos-13
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -308,6 +336,19 @@ jobs:
|
||||
- setup
|
||||
- locales-test
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
strategy:
|
||||
matrix:
|
||||
license_type:
|
||||
- build_prefix: ""
|
||||
artifact_prefix: ""
|
||||
archive_name_prefix: ""
|
||||
npm_command_prefix: "dist:"
|
||||
readable: "open source license"
|
||||
- build_prefix: "bit-"
|
||||
artifact_prefix: "bit-"
|
||||
archive_name_prefix: "bit-"
|
||||
npm_command_prefix: "dist:bit:"
|
||||
readable: "commercial license"
|
||||
env:
|
||||
_BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
@@ -433,21 +474,21 @@ jobs:
|
||||
npm link ../sdk-internal
|
||||
|
||||
- name: Build Safari extension
|
||||
run: npm run dist:safari
|
||||
run: npm run ${{matrix.license_type.npm_command_prefix}}safari
|
||||
working-directory: apps/browser
|
||||
|
||||
- name: Zip Safari build artifact
|
||||
run: |
|
||||
cd apps/browser/dist
|
||||
zip dist-safari.zip ./Safari/**/build/Release/safari.appex -r
|
||||
zip ${{matrix.license_type.archive_name_prefix }}dist-safari.zip ./Safari/**/build/Release/safari.appex -r
|
||||
pwd
|
||||
ls -la
|
||||
|
||||
- name: Upload Safari artifact
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: dist-safari-${{ env._BUILD_NUMBER }}.zip
|
||||
path: apps/browser/dist/dist-safari.zip
|
||||
name: ${{matrix.license_type.archive_name_prefix}}dist-safari-${{ env._BUILD_NUMBER }}.zip
|
||||
path: apps/browser/dist/${{matrix.license_type.archive_name_prefix}}dist-safari.zip
|
||||
if-no-files-found: error
|
||||
|
||||
crowdin-push:
|
||||
|
||||
1104
.github/workflows/release-desktop-beta.yml
vendored
1104
.github/workflows/release-desktop-beta.yml
vendored
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@ const config: StorybookConfig = {
|
||||
"../libs/auth/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../libs/dirt/card/src/**/*.mdx",
|
||||
"../libs/dirt/card/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../libs/pricing/src/**/*.mdx",
|
||||
"../libs/pricing/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../libs/tools/send/send-ui/src/**/*.mdx",
|
||||
"../libs/tools/send/send-ui/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../libs/vault/src/**/*.mdx",
|
||||
|
||||
@@ -3,32 +3,58 @@
|
||||
"version": "2025.8.2",
|
||||
"scripts": {
|
||||
"build": "npm run build:chrome",
|
||||
"build:bit": "npm run build:bit:chrome",
|
||||
"build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
|
||||
"build:bit:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js",
|
||||
"build:edge": "cross-env BROWSER=edge MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
|
||||
"build:bit:edge": "cross-env BROWSER=edge MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js",
|
||||
"build:firefox": "cross-env BROWSER=firefox NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
|
||||
"build:bit:firefox": "cross-env BROWSER=firefox NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js",
|
||||
"build:opera": "cross-env BROWSER=opera MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
|
||||
"build:bit:opera": "cross-env BROWSER=opera MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js",
|
||||
"build:safari": "cross-env BROWSER=safari NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
|
||||
"build:bit:safari": "cross-env BROWSER=safari NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js",
|
||||
"build:watch": "npm run build:watch:chrome",
|
||||
"build:watch:chrome": "npm run build:chrome -- --watch",
|
||||
"build:bit:watch:chrome": "npm run build:bit:chrome -- --watch",
|
||||
"build:watch:edge": "npm run build:edge -- --watch",
|
||||
"build:bit:watch:edge": "npm run build:bit:edge -- --watch",
|
||||
"build:watch:firefox": "npm run build:firefox -- --watch",
|
||||
"build:bit:watch:firefox": "npm run build:bit:firefox -- --watch",
|
||||
"build:watch:opera": "npm run build:opera -- --watch",
|
||||
"build:bit:watch:opera": "npm run build:bit:opera -- --watch",
|
||||
"build:watch:safari": "npm run build:safari -- --watch",
|
||||
"build:watch:firefox:mv3": "cross-env MANIFEST_VERSION=3 npm run build:firefox -- --watch",
|
||||
"build:watch:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run build:safari -- --watch",
|
||||
"build:bit:watch:safari": "npm run build:bit:safari -- --watch",
|
||||
"build:watch:firefox:mv3": "cross-env MANIFEST_VERSION=3 npm run build:watch:firefox",
|
||||
"build:bit:watch:firefox:mv3": "cross-env MANIFEST_VERSION=3 npm run build:bit:watch:firefox",
|
||||
"build:watch:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run build:watch:safari",
|
||||
"build:bit:watch:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run build:bit:watch:safari",
|
||||
"build:prod:chrome": "cross-env NODE_ENV=production npm run build:chrome",
|
||||
"build:bit:prod:chrome": "cross-env NODE_ENV=production npm run build:bit:chrome",
|
||||
"build:prod:edge": "cross-env NODE_ENV=production npm run build:edge",
|
||||
"build:bit:prod:edge": "cross-env NODE_ENV=production npm run build:bit:edge",
|
||||
"build:prod:firefox": "cross-env NODE_ENV=production npm run build:firefox",
|
||||
"build:bit:prod:firefox": "cross-env NODE_ENV=production npm run build:bit:firefox",
|
||||
"build:prod:opera": "cross-env NODE_ENV=production npm run build:opera",
|
||||
"build:bit:prod:opera": "cross-env NODE_ENV=production npm run build:bit:opera",
|
||||
"build:prod:safari": "cross-env NODE_ENV=production npm run build:safari",
|
||||
"build:bit:prod:safari": "cross-env NODE_ENV=production npm run build:bit:safari",
|
||||
"dist:chrome": "npm run build:prod:chrome && mkdir -p dist && ./scripts/compress.sh dist-chrome.zip",
|
||||
"dist:bit:chrome": "npm run build:bit:prod:chrome && mkdir -p dist && ./scripts/compress.sh bit-dist-chrome.zip",
|
||||
"dist:edge": "npm run build:prod:edge && mkdir -p dist && ./scripts/compress.sh dist-edge.zip",
|
||||
"dist:bit:edge": "npm run build:bit:prod:edge && mkdir -p dist && ./scripts/compress.sh bit-dist-edge.zip",
|
||||
"dist:firefox": "npm run build:prod:firefox && mkdir -p dist && ./scripts/compress.sh dist-firefox.zip",
|
||||
"dist:bit:firefox": "npm run build:bit:prod:firefox && mkdir -p dist && ./scripts/compress.sh bit-dist-firefox.zip",
|
||||
"dist:opera": "npm run build:prod:opera && mkdir -p dist && ./scripts/compress.sh dist-opera.zip",
|
||||
"dist:bit:opera": "npm run build:bit:prod:opera && mkdir -p dist && ./scripts/compress.sh bit-dist-opera.zip",
|
||||
"dist:safari": "npm run build:prod:safari && ./scripts/package-safari.ps1",
|
||||
"dist:bit:safari": "npm run build:bit:prod:safari && ./scripts/package-safari.ps1",
|
||||
"dist:firefox:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:firefox",
|
||||
"dist:bit:firefox:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:bit:firefox",
|
||||
"dist:opera:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:opera",
|
||||
"dist:bit:opera:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:bit:opera",
|
||||
"dist:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:safari",
|
||||
"dist:bit:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:bit:safari",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:watch:all": "jest --watchAll",
|
||||
|
||||
@@ -5588,8 +5588,14 @@
|
||||
"showLess": {
|
||||
"message": "Show less"
|
||||
},
|
||||
"next": {
|
||||
"message": "Next"
|
||||
},
|
||||
"moreBreadcrumbs": {
|
||||
"message": "More breadcrumbs",
|
||||
"description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed."
|
||||
},
|
||||
"confirmKeyConnectorDomain": {
|
||||
"message": "Confirm Key Connector domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,10 +158,13 @@ import { ApiService } from "@bitwarden/common/services/api.service";
|
||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||
import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider";
|
||||
import { buildExtensionRegistry } from "@bitwarden/common/tools/extension/factory";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/password-strength";
|
||||
import { createSystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
|
||||
import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
|
||||
@@ -302,6 +305,7 @@ import { OffscreenStorageService } from "../platform/storage/offscreen-storage.s
|
||||
import { SyncServiceListener } from "../platform/sync/sync-service.listener";
|
||||
import { BrowserSystemNotificationService } from "../platform/system-notifications/browser-system-notification.service";
|
||||
import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging";
|
||||
import { AtRiskCipherBadgeUpdaterService } from "../vault/services/at-risk-cipher-badge-updater.service";
|
||||
|
||||
import CommandsBackground from "./commands.background";
|
||||
import IdleBackground from "./idle.background";
|
||||
@@ -433,6 +437,7 @@ export default class MainBackground {
|
||||
badgeService: BadgeService;
|
||||
authStatusBadgeUpdaterService: AuthStatusBadgeUpdaterService;
|
||||
autofillBadgeUpdaterService: AutofillBadgeUpdaterService;
|
||||
atRiskCipherUpdaterService: AtRiskCipherBadgeUpdaterService;
|
||||
|
||||
onUpdatedRan: boolean;
|
||||
onReplacedRan: boolean;
|
||||
@@ -735,6 +740,7 @@ export default class MainBackground {
|
||||
this.logService,
|
||||
(logoutReason: LogoutReason, userId?: UserId) => this.logout(logoutReason, userId),
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.accountService,
|
||||
{ createRequest: (url, request) => new Request(url, request) },
|
||||
);
|
||||
|
||||
@@ -783,6 +789,7 @@ export default class MainBackground {
|
||||
this.kdfConfigService,
|
||||
this.keyService,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
);
|
||||
|
||||
this.passwordStrengthService = new PasswordStrengthService();
|
||||
@@ -841,7 +848,7 @@ export default class MainBackground {
|
||||
this.tokenService,
|
||||
);
|
||||
|
||||
this.configApiService = new ConfigApiService(this.apiService, this.tokenService);
|
||||
this.configApiService = new ConfigApiService(this.apiService);
|
||||
|
||||
this.configService = new DefaultConfigService(
|
||||
this.configApiService,
|
||||
@@ -1052,8 +1059,16 @@ export default class MainBackground {
|
||||
this.encryptService,
|
||||
this.pinService,
|
||||
this.accountService,
|
||||
this.sdkService,
|
||||
this.restrictedItemTypesService,
|
||||
createSystemServiceProvider(
|
||||
new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService),
|
||||
this.stateProvider,
|
||||
this.policyService,
|
||||
buildExtensionRegistry(),
|
||||
this.logService,
|
||||
this.platformUtilsService,
|
||||
this.configService,
|
||||
),
|
||||
);
|
||||
|
||||
this.individualVaultExportService = new IndividualVaultExportService(
|
||||
@@ -1838,6 +1853,14 @@ export default class MainBackground {
|
||||
this.logService,
|
||||
);
|
||||
|
||||
this.atRiskCipherUpdaterService = new AtRiskCipherBadgeUpdaterService(
|
||||
this.badgeService,
|
||||
this.accountService,
|
||||
this.cipherService,
|
||||
this.logService,
|
||||
this.taskService,
|
||||
);
|
||||
|
||||
this.tabsBackground = new TabsBackground(
|
||||
this,
|
||||
this.notificationBackground,
|
||||
@@ -1847,6 +1870,7 @@ export default class MainBackground {
|
||||
await this.overlayBackground.init();
|
||||
await this.tabsBackground.init();
|
||||
await this.autofillBadgeUpdaterService.init();
|
||||
await this.atRiskCipherUpdaterService.init();
|
||||
}
|
||||
|
||||
generatePassword = async (): Promise<string> => {
|
||||
|
||||
BIN
apps/browser/src/images/berry19.png
Normal file
BIN
apps/browser/src/images/berry19.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/browser/src/images/berry38.png
Normal file
BIN
apps/browser/src/images/berry38.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,4 +1,8 @@
|
||||
export const BadgeIcon = {
|
||||
Berry: {
|
||||
19: "/images/berry19.png",
|
||||
38: "/images/berry38.png",
|
||||
},
|
||||
LoggedOut: {
|
||||
19: "/images/icon19_gray.png",
|
||||
38: "/images/icon38_gray.png",
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AnonLayoutWrapperData } from "@bitwarden/components";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
|
||||
import { fido2AuthGuard } from "../auth/popup/guards/fido2-auth.guard";
|
||||
@@ -598,6 +598,24 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "confirm-key-connector-domain",
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
canActivate: [],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: ConfirmKeyConnectorDomainComponent,
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "confirmKeyConnectorDomain",
|
||||
},
|
||||
showBackButton: true,
|
||||
} satisfies ExtensionAnonLayoutWrapperData,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "tabs",
|
||||
component: TabsV2Component,
|
||||
|
||||
20
apps/browser/src/popup/app.component.html
Normal file
20
apps/browser/src/popup/app.component.html
Normal file
@@ -0,0 +1,20 @@
|
||||
@if (showSdkWarning | async) {
|
||||
<div class="tw-h-screen tw-flex tw-justify-center tw-items-center tw-p-4">
|
||||
<bit-callout type="danger">
|
||||
{{ "wasmNotSupported" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/help/wasm-not-supported/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ "learnMore" | i18n }}
|
||||
</a>
|
||||
</bit-callout>
|
||||
</div>
|
||||
} @else {
|
||||
<div [@routerTransition]="getRouteElevation(outlet)">
|
||||
<router-outlet #outlet="outlet"></router-outlet>
|
||||
</div>
|
||||
<bit-toast-container></bit-toast-container>
|
||||
}
|
||||
@@ -57,28 +57,7 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn
|
||||
selector: "app-root",
|
||||
styles: [],
|
||||
animations: [routerTransition],
|
||||
template: `
|
||||
@if (showSdkWarning | async) {
|
||||
<div class="tw-h-screen tw-flex tw-justify-center tw-items-center tw-p-4">
|
||||
<bit-callout type="danger">
|
||||
{{ "wasmNotSupported" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/help/wasm-not-supported/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ "learnMore" | i18n }}
|
||||
</a>
|
||||
</bit-callout>
|
||||
</div>
|
||||
} @else {
|
||||
<div [@routerTransition]="getRouteElevation(outlet)">
|
||||
<router-outlet #outlet="outlet"></router-outlet>
|
||||
</div>
|
||||
<bit-toast-container></bit-toast-container>
|
||||
}
|
||||
`,
|
||||
templateUrl: "app.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
@@ -4,13 +4,10 @@ import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
import { PopupSizeService } from "../platform/popup/layout/popup-size.service";
|
||||
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require("./scss/popup.scss");
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require("./scss/tailwind.css");
|
||||
|
||||
import { AppModule } from "./app.module";
|
||||
|
||||
import "./scss";
|
||||
|
||||
// We put these first to minimize the delay in window changing.
|
||||
PopupSizeService.initBodyWidthFromLocalStorage();
|
||||
// Should be removed once we deprecate support for Safari 16.0 and older. See Jira ticket [PM-1861]
|
||||
|
||||
4
apps/browser/src/popup/scss/index.ts
Normal file
4
apps/browser/src/popup/scss/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require("./popup.scss");
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require("./tailwind.css");
|
||||
@@ -43,11 +43,13 @@ import {
|
||||
AccountService,
|
||||
AccountService as AccountServiceAbstraction,
|
||||
} from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/auth-request-answering.service";
|
||||
import {
|
||||
AutofillSettingsService,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
@@ -66,7 +68,10 @@ import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
MasterPasswordServiceAbstraction,
|
||||
} from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import {
|
||||
VaultTimeoutService,
|
||||
@@ -466,6 +471,19 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: InlineDerivedStateProvider,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AuthRequestAnsweringServiceAbstraction,
|
||||
useClass: AuthRequestAnsweringService,
|
||||
deps: [
|
||||
AccountServiceAbstraction,
|
||||
ActionsService,
|
||||
AuthService,
|
||||
I18nServiceAbstraction,
|
||||
MasterPasswordServiceAbstraction,
|
||||
PlatformUtilsService,
|
||||
SystemNotificationsService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AutofillSettingsServiceAbstraction,
|
||||
useClass: AutofillSettingsService,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SecurityTask, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { BadgeService } from "../../platform/badge/badge.service";
|
||||
import { BadgeIcon } from "../../platform/badge/icon";
|
||||
import { BadgeStatePriority } from "../../platform/badge/priority";
|
||||
import { Unset } from "../../platform/badge/state";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
|
||||
import { AtRiskCipherBadgeUpdaterService } from "./at-risk-cipher-badge-updater.service";
|
||||
|
||||
describe("AtRiskCipherBadgeUpdaterService", () => {
|
||||
let service: AtRiskCipherBadgeUpdaterService;
|
||||
|
||||
let setState: jest.Mock;
|
||||
let clearState: jest.Mock;
|
||||
let warning: jest.Mock;
|
||||
let getAllDecryptedForUrl: jest.Mock;
|
||||
let getTab: jest.Mock;
|
||||
let addListener: jest.Mock;
|
||||
|
||||
const activeAccount$ = new BehaviorSubject({ id: "test-account-id" });
|
||||
const cipherViews$ = new BehaviorSubject([]);
|
||||
const pendingTasks$ = new BehaviorSubject<SecurityTask[]>([]);
|
||||
const userId = "test-user-id" as UserId;
|
||||
|
||||
beforeEach(async () => {
|
||||
setState = jest.fn().mockResolvedValue(undefined);
|
||||
clearState = jest.fn().mockResolvedValue(undefined);
|
||||
warning = jest.fn();
|
||||
getAllDecryptedForUrl = jest.fn().mockResolvedValue([]);
|
||||
getTab = jest.fn();
|
||||
addListener = jest.fn();
|
||||
|
||||
jest.spyOn(BrowserApi, "addListener").mockImplementation(addListener);
|
||||
jest.spyOn(BrowserApi, "getTab").mockImplementation(getTab);
|
||||
|
||||
service = new AtRiskCipherBadgeUpdaterService(
|
||||
{ setState, clearState } as unknown as BadgeService,
|
||||
{ activeAccount$ } as unknown as AccountService,
|
||||
{ cipherViews$, getAllDecryptedForUrl } as unknown as CipherService,
|
||||
{ warning } as unknown as LogService,
|
||||
{ pendingTasks$ } as unknown as TaskService,
|
||||
);
|
||||
|
||||
await service.init();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("clears the tab state when there are no ciphers and no pending tasks", async () => {
|
||||
const tab = { id: 1 } as chrome.tabs.Tab;
|
||||
|
||||
await service["setTabState"](tab, userId, []);
|
||||
|
||||
expect(clearState).toHaveBeenCalledWith("at-risk-cipher-badge-1");
|
||||
});
|
||||
|
||||
it("sets state when there are pending tasks for the tab", async () => {
|
||||
const tab = { id: 3, url: "https://bitwarden.com" } as chrome.tabs.Tab;
|
||||
const pendingTasks: SecurityTask[] = [{ id: "task1", cipherId: "cipher1" } as SecurityTask];
|
||||
getAllDecryptedForUrl.mockResolvedValueOnce([{ id: "cipher1" }]);
|
||||
|
||||
await service["setTabState"](tab, userId, pendingTasks);
|
||||
|
||||
expect(setState).toHaveBeenCalledWith(
|
||||
"at-risk-cipher-badge-3",
|
||||
BadgeStatePriority.High,
|
||||
{
|
||||
icon: BadgeIcon.Berry,
|
||||
text: Unset,
|
||||
backgroundColor: Unset,
|
||||
},
|
||||
3,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
import { combineLatest, map, mergeMap, of, Subject, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
|
||||
import { BadgeService } from "../../platform/badge/badge.service";
|
||||
import { BadgeIcon } from "../../platform/badge/icon";
|
||||
import { BadgeStatePriority } from "../../platform/badge/priority";
|
||||
import { Unset } from "../../platform/badge/state";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
|
||||
const StateName = (tabId: number) => `at-risk-cipher-badge-${tabId}`;
|
||||
|
||||
export class AtRiskCipherBadgeUpdaterService {
|
||||
private tabReplaced$ = new Subject<{ addedTab: chrome.tabs.Tab; removedTabId: number }>();
|
||||
private tabUpdated$ = new Subject<chrome.tabs.Tab>();
|
||||
private tabRemoved$ = new Subject<number>();
|
||||
private tabActivated$ = new Subject<chrome.tabs.Tab>();
|
||||
|
||||
private activeUserData$ = this.accountService.activeAccount$.pipe(
|
||||
filterOutNullish(),
|
||||
switchMap((user) =>
|
||||
combineLatest([
|
||||
of(user.id),
|
||||
this.taskService
|
||||
.pendingTasks$(user.id)
|
||||
.pipe(
|
||||
map((tasks) => tasks.filter((t) => t.type === SecurityTaskType.UpdateAtRiskCredential)),
|
||||
),
|
||||
this.cipherService.cipherViews$(user.id).pipe(filterOutNullish()),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private badgeService: BadgeService,
|
||||
private accountService: AccountService,
|
||||
private cipherService: CipherService,
|
||||
private logService: LogService,
|
||||
private taskService: TaskService,
|
||||
) {
|
||||
combineLatest({
|
||||
replaced: this.tabReplaced$,
|
||||
activeUserData: this.activeUserData$,
|
||||
})
|
||||
.pipe(
|
||||
mergeMap(async ({ replaced, activeUserData: [userId, pendingTasks] }) => {
|
||||
await this.clearTabState(replaced.removedTabId);
|
||||
await this.setTabState(replaced.addedTab, userId, pendingTasks);
|
||||
}),
|
||||
)
|
||||
.subscribe(() => {});
|
||||
|
||||
combineLatest({
|
||||
tab: this.tabActivated$,
|
||||
activeUserData: this.activeUserData$,
|
||||
})
|
||||
.pipe(
|
||||
mergeMap(async ({ tab, activeUserData: [userId, pendingTasks] }) => {
|
||||
await this.setTabState(tab, userId, pendingTasks);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
combineLatest({
|
||||
tab: this.tabUpdated$,
|
||||
activeUserData: this.activeUserData$,
|
||||
})
|
||||
.pipe(
|
||||
mergeMap(async ({ tab, activeUserData: [userId, pendingTasks] }) => {
|
||||
await this.setTabState(tab, userId, pendingTasks);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.tabRemoved$
|
||||
.pipe(
|
||||
mergeMap(async (tabId) => {
|
||||
await this.clearTabState(tabId);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
init() {
|
||||
BrowserApi.addListener(chrome.tabs.onReplaced, async (addedTabId, removedTabId) => {
|
||||
const newTab = await BrowserApi.getTab(addedTabId);
|
||||
if (!newTab) {
|
||||
this.logService.warning(
|
||||
`Tab replaced event received but new tab not found (id: ${addedTabId})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.tabReplaced$.next({
|
||||
removedTabId,
|
||||
addedTab: newTab,
|
||||
});
|
||||
});
|
||||
|
||||
BrowserApi.addListener(chrome.tabs.onUpdated, (_, changeInfo, tab) => {
|
||||
if (changeInfo.url) {
|
||||
this.tabUpdated$.next(tab);
|
||||
}
|
||||
});
|
||||
|
||||
BrowserApi.addListener(chrome.tabs.onActivated, async (activeInfo) => {
|
||||
const tab = await BrowserApi.getTab(activeInfo.tabId);
|
||||
if (!tab) {
|
||||
this.logService.warning(
|
||||
`Tab activated event received but tab not found (id: ${activeInfo.tabId})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.tabActivated$.next(tab);
|
||||
});
|
||||
|
||||
BrowserApi.addListener(chrome.tabs.onRemoved, (tabId, _) => this.tabRemoved$.next(tabId));
|
||||
}
|
||||
|
||||
/** Sets the pending task state for the tab */
|
||||
private async setTabState(tab: chrome.tabs.Tab, userId: UserId, pendingTasks: SecurityTask[]) {
|
||||
if (!tab.id) {
|
||||
this.logService.warning("Tab event received but tab id is undefined");
|
||||
return;
|
||||
}
|
||||
|
||||
const ciphers = tab.url
|
||||
? await this.cipherService.getAllDecryptedForUrl(tab.url, userId, [], undefined, true)
|
||||
: [];
|
||||
|
||||
const hasPendingTasksForTab = pendingTasks.some((task) =>
|
||||
ciphers.some((cipher) => cipher.id === task.cipherId && !cipher.isDeleted),
|
||||
);
|
||||
|
||||
if (!hasPendingTasksForTab) {
|
||||
await this.clearTabState(tab.id);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.badgeService.setState(
|
||||
StateName(tab.id),
|
||||
BadgeStatePriority.High,
|
||||
{
|
||||
icon: BadgeIcon.Berry,
|
||||
// Unset text and background color to use default badge appearance
|
||||
text: Unset,
|
||||
backgroundColor: Unset,
|
||||
},
|
||||
tab.id,
|
||||
);
|
||||
}
|
||||
|
||||
/** Clears the pending task state from a tab */
|
||||
private async clearTabState(tabId: number) {
|
||||
await this.badgeService.clearState(StateName(tabId));
|
||||
}
|
||||
}
|
||||
443
apps/browser/webpack.base.js
Normal file
443
apps/browser/webpack.base.js
Normal file
@@ -0,0 +1,443 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const { AngularWebpackPlugin } = require("@ngtools/webpack");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const { TsconfigPathsPlugin } = require("tsconfig-paths-webpack-plugin");
|
||||
const configurator = require("./config/config");
|
||||
const manifest = require("./webpack/manifest");
|
||||
const AngularCheckPlugin = require("./webpack/angular-check");
|
||||
|
||||
module.exports.getEnv = function getEnv() {
|
||||
const ENV = (process.env.ENV = process.env.NODE_ENV);
|
||||
const manifestVersion = process.env.MANIFEST_VERSION == 3 ? 3 : 2;
|
||||
const browser = process.env.BROWSER ?? "chrome";
|
||||
|
||||
return { ENV, manifestVersion, browser };
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* configName: string;
|
||||
* popup: {
|
||||
* entry: string;
|
||||
* entryModule: string;
|
||||
* };
|
||||
* background: {
|
||||
* entry: string;
|
||||
* };
|
||||
* tsConfig: string;
|
||||
* additionalEntries?: { [outputPath: string]: string }
|
||||
* }} params - The input parameters for building the config.
|
||||
*/
|
||||
module.exports.buildConfig = function buildConfig(params) {
|
||||
if (process.env.NODE_ENV == null) {
|
||||
process.env.NODE_ENV = "development";
|
||||
}
|
||||
|
||||
const { ENV, manifestVersion, browser } = module.exports.getEnv();
|
||||
|
||||
console.log(`Building Manifest Version ${manifestVersion} app - ${params.configName} version`);
|
||||
|
||||
const envConfig = configurator.load(ENV);
|
||||
configurator.log(envConfig);
|
||||
|
||||
const moduleRules = [
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
loader: "html-loader",
|
||||
},
|
||||
{
|
||||
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
|
||||
exclude: /loading.svg/,
|
||||
generator: {
|
||||
filename: "popup/fonts/[name].[contenthash][ext]",
|
||||
},
|
||||
type: "asset/resource",
|
||||
},
|
||||
{
|
||||
test: /\.(jpe?g|png|gif|svg)$/i,
|
||||
exclude: /.*(bwi-font|glyphicons-halflings-regular)\.svg/,
|
||||
generator: {
|
||||
filename: "popup/images/[name][ext]",
|
||||
},
|
||||
type: "asset/resource",
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
},
|
||||
"css-loader",
|
||||
"resolve-url-loader",
|
||||
{
|
||||
loader: "postcss-loader",
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
},
|
||||
"css-loader",
|
||||
"resolve-url-loader",
|
||||
{
|
||||
loader: "sass-loader",
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.[cm]?js$/,
|
||||
use: [
|
||||
{
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
configFile: "../../babel.config.json",
|
||||
cacheDirectory: ENV === "development",
|
||||
compact: ENV !== "development",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.[jt]sx?$/,
|
||||
loader: "@ngtools/webpack",
|
||||
},
|
||||
];
|
||||
|
||||
const requiredPlugins = [
|
||||
new webpack.DefinePlugin({
|
||||
"process.env": {
|
||||
ENV: JSON.stringify(ENV),
|
||||
},
|
||||
}),
|
||||
new webpack.EnvironmentPlugin({
|
||||
FLAGS: envConfig.flags,
|
||||
DEV_FLAGS: ENV === "development" ? envConfig.devFlags : {},
|
||||
}),
|
||||
];
|
||||
|
||||
const plugins = [
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/popup/index.ejs",
|
||||
filename: "popup/index.html",
|
||||
chunks: ["popup/polyfills", "popup/vendor-angular", "popup/vendor", "popup/main"],
|
||||
browser: browser,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/notification/bar.html",
|
||||
filename: "notification/bar.html",
|
||||
chunks: ["notification/bar"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/overlay/inline-menu/pages/button/button.html",
|
||||
filename: "overlay/menu-button.html",
|
||||
chunks: ["overlay/menu-button"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/overlay/inline-menu/pages/list/list.html",
|
||||
filename: "overlay/menu-list.html",
|
||||
chunks: ["overlay/menu-list"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/overlay/inline-menu/pages/menu-container/menu-container.html",
|
||||
filename: "overlay/menu.html",
|
||||
chunks: ["overlay/menu"],
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: manifestVersion == 3 ? "./src/manifest.v3.json" : "./src/manifest.json",
|
||||
to: "manifest.json",
|
||||
transform: manifest.transform(browser),
|
||||
},
|
||||
{ from: "./src/managed_schema.json", to: "managed_schema.json" },
|
||||
{ from: "./src/_locales", to: "_locales" },
|
||||
{ from: "./src/images", to: "images" },
|
||||
{ from: "./src/popup/images", to: "popup/images" },
|
||||
{ from: "./src/autofill/content/autofill.css", to: "content" },
|
||||
],
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "[name].css",
|
||||
chunkFilename: "chunk-[id].css",
|
||||
}),
|
||||
new AngularWebpackPlugin({
|
||||
tsconfig: params.tsConfig,
|
||||
entryModule: params.popup.entryModule,
|
||||
sourceMap: true,
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
process: "process/browser.js",
|
||||
}),
|
||||
new webpack.SourceMapDevToolPlugin({
|
||||
exclude: [/content\/.*/, /notification\/.*/, /overlay\/.*/],
|
||||
filename: "[file].map",
|
||||
}),
|
||||
...requiredPlugins,
|
||||
];
|
||||
|
||||
/**
|
||||
* @type {import("webpack").Configuration}
|
||||
* This config compiles everything but the background
|
||||
*/
|
||||
const mainConfig = {
|
||||
name: "main",
|
||||
mode: ENV,
|
||||
devtool: false,
|
||||
entry: {
|
||||
"popup/polyfills": "./src/popup/polyfills.ts",
|
||||
"popup/main": params.popup.entry,
|
||||
"content/trigger-autofill-script-injection":
|
||||
"./src/autofill/content/trigger-autofill-script-injection.ts",
|
||||
"content/bootstrap-autofill": "./src/autofill/content/bootstrap-autofill.ts",
|
||||
"content/bootstrap-autofill-overlay": "./src/autofill/content/bootstrap-autofill-overlay.ts",
|
||||
"content/bootstrap-autofill-overlay-menu":
|
||||
"./src/autofill/content/bootstrap-autofill-overlay-menu.ts",
|
||||
"content/bootstrap-autofill-overlay-notifications":
|
||||
"./src/autofill/content/bootstrap-autofill-overlay-notifications.ts",
|
||||
"content/autofiller": "./src/autofill/content/autofiller.ts",
|
||||
"content/auto-submit-login": "./src/autofill/content/auto-submit-login.ts",
|
||||
"content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts",
|
||||
"content/content-message-handler": "./src/autofill/content/content-message-handler.ts",
|
||||
"content/fido2-content-script": "./src/autofill/fido2/content/fido2-content-script.ts",
|
||||
"content/fido2-page-script": "./src/autofill/fido2/content/fido2-page-script.ts",
|
||||
"content/ipc-content-script": "./src/platform/ipc/content/ipc-content-script.ts",
|
||||
"notification/bar": "./src/autofill/notification/bar.ts",
|
||||
"overlay/menu-button":
|
||||
"./src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts",
|
||||
"overlay/menu-list":
|
||||
"./src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts",
|
||||
"overlay/menu":
|
||||
"./src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts",
|
||||
"content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts",
|
||||
"content/send-popup-open-message": "./src/vault/content/send-popup-open-message.ts",
|
||||
...params.additionalEntries,
|
||||
},
|
||||
cache:
|
||||
ENV !== "development"
|
||||
? false
|
||||
: {
|
||||
type: "filesystem",
|
||||
name: "main-cache",
|
||||
cacheDirectory: path.resolve(
|
||||
__dirname,
|
||||
"../../node_modules/.cache/webpack-browser-main",
|
||||
),
|
||||
buildDependencies: {
|
||||
config: [__filename],
|
||||
},
|
||||
},
|
||||
snapshot: {
|
||||
unmanagedPaths: [path.resolve(__dirname, "../../node_modules/@bitwarden/")],
|
||||
},
|
||||
optimization: {
|
||||
minimize: ENV !== "development",
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
exclude: [/content\/.*/, /notification\/.*/, /overlay\/.*/],
|
||||
terserOptions: {
|
||||
// Replicate Angular CLI behaviour
|
||||
compress: {
|
||||
global_defs: {
|
||||
ngDevMode: false,
|
||||
ngI18nClosureMode: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
commons: {
|
||||
test(module, chunks) {
|
||||
return (
|
||||
module.resource != null &&
|
||||
module.resource.includes(`${path.sep}node_modules${path.sep}`) &&
|
||||
!module.resource.includes(`${path.sep}node_modules${path.sep}@angular${path.sep}`)
|
||||
);
|
||||
},
|
||||
name: "popup/vendor",
|
||||
chunks: (chunk) => {
|
||||
return chunk.name === "popup/main";
|
||||
},
|
||||
},
|
||||
angular: {
|
||||
test(module, chunks) {
|
||||
return (
|
||||
module.resource != null &&
|
||||
module.resource.includes(`${path.sep}node_modules${path.sep}@angular${path.sep}`)
|
||||
);
|
||||
},
|
||||
name: "popup/vendor-angular",
|
||||
chunks: (chunk) => {
|
||||
return chunk.name === "popup/main";
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
symlinks: false,
|
||||
modules: [path.resolve("../../node_modules")],
|
||||
fallback: {
|
||||
assert: false,
|
||||
buffer: require.resolve("buffer/"),
|
||||
util: require.resolve("util/"),
|
||||
url: require.resolve("url/"),
|
||||
fs: false,
|
||||
path: require.resolve("path-browserify"),
|
||||
},
|
||||
cache: true,
|
||||
},
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
chunkFilename: "assets/[name].js",
|
||||
webassemblyModuleFilename: "assets/[modulehash].wasm",
|
||||
path: path.resolve(__dirname, "build"),
|
||||
clean: true,
|
||||
},
|
||||
module: {
|
||||
rules: moduleRules,
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
plugins: plugins,
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import("webpack").Configuration[]}
|
||||
*/
|
||||
const configs = [];
|
||||
|
||||
if (manifestVersion == 2) {
|
||||
mainConfig.optimization.splitChunks.cacheGroups.commons2 = {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: "vendor",
|
||||
chunks: (chunk) => {
|
||||
return chunk.name === "background";
|
||||
},
|
||||
};
|
||||
|
||||
// Manifest V2 uses Background Pages which requires a html page.
|
||||
mainConfig.plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/platform/background.html",
|
||||
filename: "background.html",
|
||||
chunks: ["vendor", "background"],
|
||||
}),
|
||||
);
|
||||
|
||||
// Manifest V2 background pages can be run through the regular build pipeline.
|
||||
// Since it's a standard webpage.
|
||||
mainConfig.entry.background = params.background.entry;
|
||||
mainConfig.entry["content/fido2-page-script-delay-append-mv2"] =
|
||||
"./src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts";
|
||||
|
||||
configs.push(mainConfig);
|
||||
} else {
|
||||
// Firefox does not use the offscreen API
|
||||
if (browser !== "firefox") {
|
||||
mainConfig.entry["offscreen-document/offscreen-document"] =
|
||||
"./src/platform/offscreen-document/offscreen-document.ts";
|
||||
|
||||
mainConfig.plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/platform/offscreen-document/index.html",
|
||||
filename: "offscreen-document/index.html",
|
||||
chunks: ["offscreen-document/offscreen-document"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const target = browser === "firefox" ? "web" : "webworker";
|
||||
|
||||
/**
|
||||
* @type {import("webpack").Configuration}
|
||||
*/
|
||||
const backgroundConfig = {
|
||||
name: "background",
|
||||
mode: ENV,
|
||||
devtool: false,
|
||||
entry: params.background.entry,
|
||||
target: target,
|
||||
output: {
|
||||
filename: "background.js",
|
||||
path: path.resolve(__dirname, "build"),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: "ts-loader",
|
||||
},
|
||||
],
|
||||
},
|
||||
cache:
|
||||
ENV !== "development"
|
||||
? false
|
||||
: {
|
||||
type: "filesystem",
|
||||
name: "background-cache",
|
||||
cacheDirectory: path.resolve(
|
||||
__dirname,
|
||||
"../../node_modules/.cache/webpack-browser-background",
|
||||
),
|
||||
buildDependencies: {
|
||||
config: [__filename],
|
||||
},
|
||||
},
|
||||
snapshot: {
|
||||
unmanagedPaths: [path.resolve(__dirname, "../../node_modules/@bitwarden/")],
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
symlinks: false,
|
||||
modules: [path.resolve("../../node_modules")],
|
||||
plugins: [new TsconfigPathsPlugin()],
|
||||
fallback: {
|
||||
fs: false,
|
||||
path: require.resolve("path-browserify"),
|
||||
},
|
||||
cache: true,
|
||||
},
|
||||
dependencies: ["main"],
|
||||
plugins: [...requiredPlugins, new AngularCheckPlugin()],
|
||||
};
|
||||
|
||||
// Safari's desktop build process requires a background.html and vendor.js file to exist
|
||||
// within the root of the extension. This is a workaround to allow us to build Safari
|
||||
// for manifest v2 and v3 without modifying the desktop project structure.
|
||||
if (browser === "safari") {
|
||||
backgroundConfig.plugins.push(
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{ from: "./src/safari/mv3/fake-background.html", to: "background.html" },
|
||||
{ from: "./src/safari/mv3/fake-vendor.js", to: "vendor.js" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
configs.push(mainConfig);
|
||||
configs.push(backgroundConfig);
|
||||
}
|
||||
|
||||
return configs;
|
||||
};
|
||||
@@ -1,416 +1,13 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const { AngularWebpackPlugin } = require("@ngtools/webpack");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const { TsconfigPathsPlugin } = require("tsconfig-paths-webpack-plugin");
|
||||
const configurator = require("./config/config");
|
||||
const manifest = require("./webpack/manifest");
|
||||
const AngularCheckPlugin = require("./webpack/angular-check");
|
||||
const { buildConfig } = require("./webpack.base");
|
||||
|
||||
if (process.env.NODE_ENV == null) {
|
||||
process.env.NODE_ENV = "development";
|
||||
}
|
||||
const ENV = (process.env.ENV = process.env.NODE_ENV);
|
||||
const manifestVersion = process.env.MANIFEST_VERSION == 3 ? 3 : 2;
|
||||
const browser = process.env.BROWSER ?? "chrome";
|
||||
|
||||
console.log(`Building Manifest Version ${manifestVersion} app`);
|
||||
|
||||
const envConfig = configurator.load(ENV);
|
||||
configurator.log(envConfig);
|
||||
|
||||
const moduleRules = [
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
loader: "html-loader",
|
||||
},
|
||||
{
|
||||
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
|
||||
exclude: /loading.svg/,
|
||||
generator: {
|
||||
filename: "popup/fonts/[name].[contenthash][ext]",
|
||||
},
|
||||
type: "asset/resource",
|
||||
},
|
||||
{
|
||||
test: /\.(jpe?g|png|gif|svg)$/i,
|
||||
exclude: /.*(bwi-font|glyphicons-halflings-regular)\.svg/,
|
||||
generator: {
|
||||
filename: "popup/images/[name][ext]",
|
||||
},
|
||||
type: "asset/resource",
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
},
|
||||
"css-loader",
|
||||
"resolve-url-loader",
|
||||
{
|
||||
loader: "postcss-loader",
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
},
|
||||
"css-loader",
|
||||
"resolve-url-loader",
|
||||
{
|
||||
loader: "sass-loader",
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.[cm]?js$/,
|
||||
use: [
|
||||
{
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
configFile: "../../babel.config.json",
|
||||
cacheDirectory: ENV === "development",
|
||||
compact: ENV !== "development",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.[jt]sx?$/,
|
||||
loader: "@ngtools/webpack",
|
||||
},
|
||||
];
|
||||
|
||||
const requiredPlugins = [
|
||||
new webpack.DefinePlugin({
|
||||
"process.env": {
|
||||
ENV: JSON.stringify(ENV),
|
||||
},
|
||||
}),
|
||||
new webpack.EnvironmentPlugin({
|
||||
FLAGS: envConfig.flags,
|
||||
DEV_FLAGS: ENV === "development" ? envConfig.devFlags : {},
|
||||
}),
|
||||
];
|
||||
|
||||
const plugins = [
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/popup/index.ejs",
|
||||
filename: "popup/index.html",
|
||||
chunks: ["popup/polyfills", "popup/vendor-angular", "popup/vendor", "popup/main"],
|
||||
browser: browser,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/notification/bar.html",
|
||||
filename: "notification/bar.html",
|
||||
chunks: ["notification/bar"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/overlay/inline-menu/pages/button/button.html",
|
||||
filename: "overlay/menu-button.html",
|
||||
chunks: ["overlay/menu-button"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/overlay/inline-menu/pages/list/list.html",
|
||||
filename: "overlay/menu-list.html",
|
||||
chunks: ["overlay/menu-list"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/overlay/inline-menu/pages/menu-container/menu-container.html",
|
||||
filename: "overlay/menu.html",
|
||||
chunks: ["overlay/menu"],
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: manifestVersion == 3 ? "./src/manifest.v3.json" : "./src/manifest.json",
|
||||
to: "manifest.json",
|
||||
transform: manifest.transform(browser),
|
||||
},
|
||||
{ from: "./src/managed_schema.json", to: "managed_schema.json" },
|
||||
{ from: "./src/_locales", to: "_locales" },
|
||||
{ from: "./src/images", to: "images" },
|
||||
{ from: "./src/popup/images", to: "popup/images" },
|
||||
{ from: "./src/autofill/content/autofill.css", to: "content" },
|
||||
],
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "[name].css",
|
||||
chunkFilename: "chunk-[id].css",
|
||||
}),
|
||||
new AngularWebpackPlugin({
|
||||
tsConfigPath: "tsconfig.json",
|
||||
module.exports = buildConfig({
|
||||
configName: "OSS",
|
||||
popup: {
|
||||
entry: "./src/popup/main.ts",
|
||||
entryModule: "src/popup/app.module#AppModule",
|
||||
sourceMap: true,
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
process: "process/browser.js",
|
||||
}),
|
||||
new webpack.SourceMapDevToolPlugin({
|
||||
exclude: [/content\/.*/, /notification\/.*/, /overlay\/.*/],
|
||||
filename: "[file].map",
|
||||
}),
|
||||
...requiredPlugins,
|
||||
];
|
||||
|
||||
/**
|
||||
* @type {import("webpack").Configuration}
|
||||
* This config compiles everything but the background
|
||||
*/
|
||||
const mainConfig = {
|
||||
name: "main",
|
||||
mode: ENV,
|
||||
devtool: false,
|
||||
entry: {
|
||||
"popup/polyfills": "./src/popup/polyfills.ts",
|
||||
"popup/main": "./src/popup/main.ts",
|
||||
"content/trigger-autofill-script-injection":
|
||||
"./src/autofill/content/trigger-autofill-script-injection.ts",
|
||||
"content/bootstrap-autofill": "./src/autofill/content/bootstrap-autofill.ts",
|
||||
"content/bootstrap-autofill-overlay": "./src/autofill/content/bootstrap-autofill-overlay.ts",
|
||||
"content/bootstrap-autofill-overlay-menu":
|
||||
"./src/autofill/content/bootstrap-autofill-overlay-menu.ts",
|
||||
"content/bootstrap-autofill-overlay-notifications":
|
||||
"./src/autofill/content/bootstrap-autofill-overlay-notifications.ts",
|
||||
"content/autofiller": "./src/autofill/content/autofiller.ts",
|
||||
"content/auto-submit-login": "./src/autofill/content/auto-submit-login.ts",
|
||||
"content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts",
|
||||
"content/content-message-handler": "./src/autofill/content/content-message-handler.ts",
|
||||
"content/fido2-content-script": "./src/autofill/fido2/content/fido2-content-script.ts",
|
||||
"content/fido2-page-script": "./src/autofill/fido2/content/fido2-page-script.ts",
|
||||
"content/ipc-content-script": "./src/platform/ipc/content/ipc-content-script.ts",
|
||||
"notification/bar": "./src/autofill/notification/bar.ts",
|
||||
"overlay/menu-button":
|
||||
"./src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts",
|
||||
"overlay/menu-list":
|
||||
"./src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts",
|
||||
"overlay/menu":
|
||||
"./src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts",
|
||||
"content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts",
|
||||
"content/send-popup-open-message": "./src/vault/content/send-popup-open-message.ts",
|
||||
},
|
||||
cache:
|
||||
ENV !== "development"
|
||||
? false
|
||||
: {
|
||||
type: "filesystem",
|
||||
name: "main-cache",
|
||||
cacheDirectory: path.resolve(__dirname, "../../node_modules/.cache/webpack-browser-main"),
|
||||
buildDependencies: {
|
||||
config: [__filename],
|
||||
},
|
||||
},
|
||||
snapshot: {
|
||||
unmanagedPaths: [path.resolve(__dirname, "../../node_modules/@bitwarden/")],
|
||||
},
|
||||
optimization: {
|
||||
minimize: ENV !== "development",
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
exclude: [/content\/.*/, /notification\/.*/, /overlay\/.*/],
|
||||
terserOptions: {
|
||||
// Replicate Angular CLI behaviour
|
||||
compress: {
|
||||
global_defs: {
|
||||
ngDevMode: false,
|
||||
ngI18nClosureMode: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
commons: {
|
||||
test(module, chunks) {
|
||||
return (
|
||||
module.resource != null &&
|
||||
module.resource.includes(`${path.sep}node_modules${path.sep}`) &&
|
||||
!module.resource.includes(`${path.sep}node_modules${path.sep}@angular${path.sep}`)
|
||||
);
|
||||
},
|
||||
name: "popup/vendor",
|
||||
chunks: (chunk) => {
|
||||
return chunk.name === "popup/main";
|
||||
},
|
||||
},
|
||||
angular: {
|
||||
test(module, chunks) {
|
||||
return (
|
||||
module.resource != null &&
|
||||
module.resource.includes(`${path.sep}node_modules${path.sep}@angular${path.sep}`)
|
||||
);
|
||||
},
|
||||
name: "popup/vendor-angular",
|
||||
chunks: (chunk) => {
|
||||
return chunk.name === "popup/main";
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
symlinks: false,
|
||||
modules: [path.resolve("../../node_modules")],
|
||||
fallback: {
|
||||
assert: false,
|
||||
buffer: require.resolve("buffer/"),
|
||||
util: require.resolve("util/"),
|
||||
url: require.resolve("url/"),
|
||||
fs: false,
|
||||
path: require.resolve("path-browserify"),
|
||||
},
|
||||
cache: true,
|
||||
},
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
chunkFilename: "assets/[name].js",
|
||||
webassemblyModuleFilename: "assets/[modulehash].wasm",
|
||||
path: path.resolve(__dirname, "build"),
|
||||
clean: true,
|
||||
},
|
||||
module: {
|
||||
rules: moduleRules,
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
plugins: plugins,
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import("webpack").Configuration[]}
|
||||
*/
|
||||
const configs = [];
|
||||
|
||||
if (manifestVersion == 2) {
|
||||
mainConfig.optimization.splitChunks.cacheGroups.commons2 = {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: "vendor",
|
||||
chunks: (chunk) => {
|
||||
return chunk.name === "background";
|
||||
},
|
||||
};
|
||||
|
||||
// Manifest V2 uses Background Pages which requires a html page.
|
||||
mainConfig.plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/platform/background.html",
|
||||
filename: "background.html",
|
||||
chunks: ["vendor", "background"],
|
||||
}),
|
||||
);
|
||||
|
||||
// Manifest V2 background pages can be run through the regular build pipeline.
|
||||
// Since it's a standard webpage.
|
||||
mainConfig.entry.background = "./src/platform/background.ts";
|
||||
mainConfig.entry["content/fido2-page-script-delay-append-mv2"] =
|
||||
"./src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts";
|
||||
|
||||
configs.push(mainConfig);
|
||||
} else {
|
||||
// Firefox does not use the offscreen API
|
||||
if (browser !== "firefox") {
|
||||
mainConfig.entry["offscreen-document/offscreen-document"] =
|
||||
"./src/platform/offscreen-document/offscreen-document.ts";
|
||||
|
||||
mainConfig.plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/platform/offscreen-document/index.html",
|
||||
filename: "offscreen-document/index.html",
|
||||
chunks: ["offscreen-document/offscreen-document"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const target = browser === "firefox" ? "web" : "webworker";
|
||||
|
||||
/**
|
||||
* @type {import("webpack").Configuration}
|
||||
*/
|
||||
const backgroundConfig = {
|
||||
name: "background",
|
||||
mode: ENV,
|
||||
devtool: false,
|
||||
background: {
|
||||
entry: "./src/platform/background.ts",
|
||||
target: target,
|
||||
output: {
|
||||
filename: "background.js",
|
||||
path: path.resolve(__dirname, "build"),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: "ts-loader",
|
||||
},
|
||||
],
|
||||
},
|
||||
cache:
|
||||
ENV !== "development"
|
||||
? false
|
||||
: {
|
||||
type: "filesystem",
|
||||
name: "background-cache",
|
||||
cacheDirectory: path.resolve(
|
||||
__dirname,
|
||||
"../../node_modules/.cache/webpack-browser-background",
|
||||
),
|
||||
buildDependencies: {
|
||||
config: [__filename],
|
||||
},
|
||||
},
|
||||
snapshot: {
|
||||
unmanagedPaths: [path.resolve(__dirname, "../../node_modules/@bitwarden/")],
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
symlinks: false,
|
||||
modules: [path.resolve("../../node_modules")],
|
||||
plugins: [new TsconfigPathsPlugin()],
|
||||
fallback: {
|
||||
fs: false,
|
||||
path: require.resolve("path-browserify"),
|
||||
},
|
||||
cache: true,
|
||||
},
|
||||
dependencies: ["main"],
|
||||
plugins: [...requiredPlugins, new AngularCheckPlugin()],
|
||||
};
|
||||
|
||||
// Safari's desktop build process requires a background.html and vendor.js file to exist
|
||||
// within the root of the extension. This is a workaround to allow us to build Safari
|
||||
// for manifest v2 and v3 without modifying the desktop project structure.
|
||||
if (browser === "safari") {
|
||||
backgroundConfig.plugins.push(
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{ from: "./src/safari/mv3/fake-background.html", to: "background.html" },
|
||||
{ from: "./src/safari/mv3/fake-vendor.js", to: "vendor.js" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
configs.push(mainConfig);
|
||||
configs.push(backgroundConfig);
|
||||
}
|
||||
|
||||
module.exports = configs;
|
||||
},
|
||||
tsConfig: "tsconfig.json",
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { NodeUtils } from "@bitwarden/node/node-utils";
|
||||
|
||||
import { ConfirmKeyConnectorDomainCommand } from "../../key-management/confirm-key-connector-domain.command";
|
||||
import { Response } from "../../models/response";
|
||||
import { MessageResponse } from "../../models/response/message.response";
|
||||
|
||||
@@ -332,6 +333,24 @@ export class LoginCommand {
|
||||
);
|
||||
}
|
||||
|
||||
// Check if Key Connector domain confirmation is required
|
||||
const domainConfirmation = await firstValueFrom(
|
||||
this.keyConnectorService.requiresDomainConfirmation$(response.userId),
|
||||
);
|
||||
if (domainConfirmation != null) {
|
||||
const command = new ConfirmKeyConnectorDomainCommand(
|
||||
response.userId,
|
||||
domainConfirmation.keyConnectorUrl,
|
||||
this.keyConnectorService,
|
||||
this.logoutCallback,
|
||||
this.i18nService,
|
||||
);
|
||||
const confirmResponse = await command.run();
|
||||
if (!confirmResponse.success) {
|
||||
return confirmResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// Run full sync before handling success response or password reset flows (to get Master Password Policies)
|
||||
await this.syncService.fullSync(true, { skipTokenRefresh: true });
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { createPromptModule } from "inquirer";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { Response } from "../models/response";
|
||||
import { MessageResponse } from "../models/response/message.response";
|
||||
import { I18nService } from "../platform/services/i18n.service";
|
||||
|
||||
import { ConfirmKeyConnectorDomainCommand } from "./confirm-key-connector-domain.command";
|
||||
|
||||
jest.mock("inquirer", () => {
|
||||
return {
|
||||
createPromptModule: jest.fn(() => jest.fn(() => Promise.resolve({ confirm: "" }))),
|
||||
};
|
||||
});
|
||||
|
||||
describe("ConfirmKeyConnectorDomainCommand", () => {
|
||||
let command: ConfirmKeyConnectorDomainCommand;
|
||||
|
||||
const userId = "test-user-id" as UserId;
|
||||
const keyConnectorUrl = "https://keyconnector.example.com";
|
||||
|
||||
const keyConnectorService = mock<KeyConnectorService>();
|
||||
const logout = jest.fn();
|
||||
const i18nService = mock<I18nService>();
|
||||
|
||||
beforeEach(async () => {
|
||||
command = new ConfirmKeyConnectorDomainCommand(
|
||||
userId,
|
||||
keyConnectorUrl,
|
||||
keyConnectorService,
|
||||
logout,
|
||||
i18nService,
|
||||
);
|
||||
|
||||
i18nService.t.mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case "confirmKeyConnectorDomain":
|
||||
return "Please confirm the domain below with your organization administrator. Key Connector domain: https://keyconnector.example.com";
|
||||
case "confirm":
|
||||
return "Confirm";
|
||||
case "logOut":
|
||||
return "Log out";
|
||||
case "youHaveBeenLoggedOut":
|
||||
return "You have been logged out.";
|
||||
case "organizationUsingKeyConnectorConfirmLoggedOut":
|
||||
return "An organization you are a member of is using Key Connector. In order to access the vault, you must confirm the Key Connector domain now via the web vault. You have been logged out.";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("run", () => {
|
||||
it("should logout and return error response if no interaction available", async () => {
|
||||
process.env.BW_NOINTERACTION = "true";
|
||||
|
||||
const response = await command.run();
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toEqual(false);
|
||||
expect(response).toEqual(
|
||||
Response.error(
|
||||
new MessageResponse(
|
||||
"An organization you are a member of is using Key Connector. In order to access the vault, you must confirm the Key Connector domain now via the web vault. You have been logged out.",
|
||||
null,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(logout).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should logout and return error response if interaction answer is cancel", async () => {
|
||||
process.env.BW_NOINTERACTION = "false";
|
||||
|
||||
(createPromptModule as jest.Mock).mockImplementation(() =>
|
||||
jest.fn((prompt) => {
|
||||
assertPrompt(prompt);
|
||||
return Promise.resolve({ confirm: "cancel" });
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await command.run();
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toEqual(false);
|
||||
expect(response).toEqual(Response.error("You have been logged out."));
|
||||
expect(logout).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should convert new sso user to key connector and return success response if answer is confirmed", async () => {
|
||||
process.env.BW_NOINTERACTION = "false";
|
||||
|
||||
(createPromptModule as jest.Mock).mockImplementation(() =>
|
||||
jest.fn((prompt) => {
|
||||
assertPrompt(prompt);
|
||||
return Promise.resolve({ confirm: "confirmed" });
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await command.run();
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toEqual(true);
|
||||
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
|
||||
it("should logout and throw error if convert new sso user to key connector failed", async () => {
|
||||
process.env.BW_NOINTERACTION = "false";
|
||||
|
||||
(createPromptModule as jest.Mock).mockImplementation(() =>
|
||||
jest.fn((prompt) => {
|
||||
assertPrompt(prompt);
|
||||
return Promise.resolve({ confirm: "confirmed" });
|
||||
}),
|
||||
);
|
||||
|
||||
keyConnectorService.convertNewSsoUserToKeyConnector.mockRejectedValue(
|
||||
new Error("Migration failed"),
|
||||
);
|
||||
|
||||
await expect(command.run()).rejects.toThrow("Migration failed");
|
||||
expect(logout).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
function assertPrompt(prompt: unknown) {
|
||||
expect(typeof prompt).toEqual("object");
|
||||
expect(prompt).toHaveProperty("type");
|
||||
expect(prompt).toHaveProperty("name");
|
||||
expect(prompt).toHaveProperty("message");
|
||||
expect(prompt).toHaveProperty("choices");
|
||||
const promptObj = prompt as Record<string, unknown>;
|
||||
expect(promptObj["type"]).toEqual("list");
|
||||
expect(promptObj["name"]).toEqual("confirm");
|
||||
expect(promptObj["message"]).toEqual(
|
||||
`Please confirm the domain below with your organization administrator. Key Connector domain: ${keyConnectorUrl}`,
|
||||
);
|
||||
expect(promptObj["choices"]).toBeInstanceOf(Array);
|
||||
const choices = promptObj["choices"] as Array<Record<string, unknown>>;
|
||||
expect(choices).toHaveLength(2);
|
||||
expect(choices[0]).toEqual({
|
||||
name: "Confirm",
|
||||
value: "confirmed",
|
||||
});
|
||||
expect(choices[1]).toEqual({
|
||||
name: "Log out",
|
||||
value: "cancel",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as inquirer from "inquirer";
|
||||
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { Response } from "../models/response";
|
||||
import { MessageResponse } from "../models/response/message.response";
|
||||
|
||||
export class ConfirmKeyConnectorDomainCommand {
|
||||
constructor(
|
||||
private readonly userId: UserId,
|
||||
private readonly keyConnectorUrl: string,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private logout: () => Promise<void>,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async run(): Promise<Response> {
|
||||
// If no interaction available, alert user to use web vault
|
||||
const canInteract = process.env.BW_NOINTERACTION !== "true";
|
||||
if (!canInteract) {
|
||||
await this.logout();
|
||||
return Response.error(
|
||||
new MessageResponse(
|
||||
this.i18nService.t("organizationUsingKeyConnectorConfirmLoggedOut"),
|
||||
null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: "list",
|
||||
name: "confirm",
|
||||
message: this.i18nService.t("confirmKeyConnectorDomain", this.keyConnectorUrl),
|
||||
choices: [
|
||||
{
|
||||
name: this.i18nService.t("confirm"),
|
||||
value: "confirmed",
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t("logOut"),
|
||||
value: "cancel",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (answer.confirm === "confirmed") {
|
||||
try {
|
||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(this.userId);
|
||||
} catch (e) {
|
||||
await this.logout();
|
||||
throw e;
|
||||
}
|
||||
|
||||
return Response.success();
|
||||
} else {
|
||||
await this.logout();
|
||||
return Response.error(this.i18nService.t("youHaveBeenLoggedOut"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,5 +218,20 @@
|
||||
},
|
||||
"myItems": {
|
||||
"message": "My Items"
|
||||
},
|
||||
"organizationUsingKeyConnectorConfirmLoggedOut": {
|
||||
"message": "An organization you are a member of is using Key Connector. In order to access the vault, you must confirm the Key Connector domain now via the web vault. You have been logged out."
|
||||
},
|
||||
"confirmKeyConnectorDomain": {
|
||||
"message": "Please confirm the domain below with your organization administrator. Key Connector domain: $KEYCONNECTORDOMAIN$",
|
||||
"placeholders": {
|
||||
"keyConnectorDomain": {
|
||||
"content": "$1",
|
||||
"example": "Key Connector domain"
|
||||
}
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"message": "Confirm"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as FormData from "form-data";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import * as fe from "node-fetch";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
@@ -28,6 +29,7 @@ export class NodeApiService extends ApiService {
|
||||
logService: LogService,
|
||||
logoutCallback: () => Promise<void>,
|
||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
accountService: AccountService,
|
||||
customUserAgent: string = null,
|
||||
) {
|
||||
super(
|
||||
@@ -39,6 +41,7 @@ export class NodeApiService extends ApiService {
|
||||
logService,
|
||||
logoutCallback,
|
||||
vaultTimeoutSettingsService,
|
||||
accountService,
|
||||
{ createRequest: (url, request) => new Request(url, request) },
|
||||
customUserAgent,
|
||||
);
|
||||
|
||||
@@ -113,10 +113,13 @@ import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal";
|
||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||
import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider";
|
||||
import { buildExtensionRegistry } from "@bitwarden/common/tools/extension/factory";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/password-strength";
|
||||
import { createSystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
|
||||
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
|
||||
@@ -504,12 +507,13 @@ export class ServiceContainer {
|
||||
this.logService,
|
||||
logoutCallback,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.accountService,
|
||||
customUserAgent,
|
||||
);
|
||||
|
||||
this.containerService = new ContainerService(this.keyService, this.encryptService);
|
||||
|
||||
this.configApiService = new ConfigApiService(this.apiService, this.tokenService);
|
||||
this.configApiService = new ConfigApiService(this.apiService);
|
||||
|
||||
this.authService = new AuthService(
|
||||
this.accountService,
|
||||
@@ -601,6 +605,7 @@ export class ServiceContainer {
|
||||
this.kdfConfigService,
|
||||
this.keyService,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
customUserAgent,
|
||||
);
|
||||
|
||||
@@ -814,8 +819,16 @@ export class ServiceContainer {
|
||||
this.encryptService,
|
||||
this.pinService,
|
||||
this.accountService,
|
||||
this.sdkService,
|
||||
this.restrictedItemTypesService,
|
||||
createSystemServiceProvider(
|
||||
new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService),
|
||||
this.stateProvider,
|
||||
this.policyService,
|
||||
buildExtensionRegistry(),
|
||||
this.logService,
|
||||
this.platformUtilsService,
|
||||
this.configService,
|
||||
),
|
||||
);
|
||||
|
||||
this.individualExportService = new IndividualVaultExportService(
|
||||
|
||||
132
apps/desktop/desktop_native/Cargo.lock
generated
132
apps/desktop/desktop_native/Cargo.lock
generated
@@ -447,6 +447,32 @@ dependencies = [
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitwarden_chromium_importer"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"cbc",
|
||||
"hex",
|
||||
"homedir",
|
||||
"log",
|
||||
"oo7",
|
||||
"pbkdf2",
|
||||
"rand 0.9.1",
|
||||
"rusqlite",
|
||||
"security-framework",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"tokio",
|
||||
"winapi",
|
||||
"windows 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -586,9 +612,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.38"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
|
||||
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -596,9 +622,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.38"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
|
||||
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -608,9 +634,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.32"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -897,6 +923,7 @@ dependencies = [
|
||||
"rsa",
|
||||
"russh-cryptovec",
|
||||
"scopeguard",
|
||||
"secmem-proc",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"sha2",
|
||||
@@ -923,6 +950,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"autotype",
|
||||
"base64",
|
||||
"bitwarden_chromium_importer",
|
||||
"desktop_core",
|
||||
"hex",
|
||||
"log",
|
||||
@@ -1166,6 +1194,18 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
@@ -1425,6 +1465,18 @@ name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -1690,6 +1742,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.10"
|
||||
@@ -2643,6 +2706,20 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "russh-cryptovec"
|
||||
version = "0.7.3"
|
||||
@@ -2694,6 +2771,16 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix-linux-procfs"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"rustix 1.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
@@ -2772,6 +2859,21 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secmem-proc"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "473559b1d28f530c3a9b5f91a2866053e2b1c528a0e43dae83048139c99490c2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"rustix 1.0.7",
|
||||
"rustix-linux-procfs",
|
||||
"thiserror 2.0.12",
|
||||
"windows 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.1.0"
|
||||
@@ -2847,6 +2949,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
@@ -3179,6 +3292,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
@@ -3497,6 +3611,12 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"autotype",
|
||||
"bitwarden_chromium_importer",
|
||||
"core",
|
||||
"macos_provider",
|
||||
"napi",
|
||||
@@ -21,7 +22,6 @@ anyhow = "=1.0.94"
|
||||
arboard = { version = "=3.6.0", default-features = false }
|
||||
ashpd = "=0.11.0"
|
||||
base64 = "=0.22.1"
|
||||
bindgen = "=0.72.0"
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "a641316227227f8777fdf56ac9fa2d6b5f7fe662" }
|
||||
byteorder = "=1.5.0"
|
||||
bytes = "=1.10.1"
|
||||
@@ -49,6 +49,7 @@ rand = "=0.9.1"
|
||||
rsa = "=0.9.6"
|
||||
russh-cryptovec = "=0.7.3"
|
||||
scopeguard = "=1.2.0"
|
||||
secmem-proc = "=0.3.7"
|
||||
security-framework = "=3.1.0"
|
||||
security-framework-sys = "=2.13.0"
|
||||
serde = "=1.0.209"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "bitwarden_chromium_importer"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
version = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
aes = { workspace = true }
|
||||
aes-gcm = "=0.10.3"
|
||||
anyhow = { workspace = true }
|
||||
async-trait = "=0.1.88"
|
||||
base64 = { workspace = true }
|
||||
cbc = { workspace = true, features = ["alloc"] }
|
||||
hex = { workspace = true }
|
||||
homedir = { workspace = true }
|
||||
log = { workspace = true }
|
||||
pbkdf2 = "=0.12.2"
|
||||
rand = { workspace = true }
|
||||
rusqlite = { version = "=0.35.0", features = ["bundled"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sha1 = "=0.10.6"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
security-framework = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
winapi = { version = "=0.3.9", features = ["dpapi", "memoryapi"] }
|
||||
windows = { workspace = true, features = ["Win32_Security", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_Services", "Win32_System_Threading", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
oo7 = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
# Windows ABE Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
The Windows Application Bound Encryption (ABE) consists of three main components that work together:
|
||||
|
||||
- **client library** -- Library that is part of the desktop client application
|
||||
- **admin.exe** -- Service launcher running as ADMINISTRATOR
|
||||
- **service.exe** -- Background Windows service running as SYSTEM
|
||||
|
||||
_(The names of the binaries will be changed for the released product.)_
|
||||
|
||||
## The goal
|
||||
|
||||
The goal of this subsystem is to decrypt the master encryption key with which the login information
|
||||
is encrypted on the local system in Windows. This applies to the most recent versions of Chrome and
|
||||
Edge (untested yet) that are using the ABE/v20 encryption scheme for some of the local profiles.
|
||||
|
||||
The general idea of this encryption scheme is that Chrome generates a unique random encryption key,
|
||||
then encrypts it at the user level with a fixed key. It then sends it to the Windows Data Protection
|
||||
API at the user level, and then, using an installed service, encrypts it with the Windows Data
|
||||
Protection API at the system level on top of that. This triply encrypted key is later stored in the
|
||||
`Local State` file.
|
||||
|
||||
The next paragraphs describe what is done at each level to decrypt the key.
|
||||
|
||||
## 1. Client library
|
||||
|
||||
This is a Rust module that is part of the Chromium importer. It only compiles and runs on Windows
|
||||
(see `abe.rs` and `abe_config.rs`). Its main task is to launch `admin.exe` with elevated privileges
|
||||
by presenting the user with the UAC screen. See the `abe::decrypt_with_admin_and_service` invocation
|
||||
in `windows.rs`.
|
||||
|
||||
This function takes three arguments:
|
||||
|
||||
1. Absolute path to `admin.exe`
|
||||
2. Absolute path to `service.exe`
|
||||
3. Base64 string of the ABE key extracted from the browser's local state
|
||||
|
||||
It's not possible to install the service from the user-level executable. So first, we have to
|
||||
elevate the privileges and run `admin.exe` as ADMINISTRATOR. This is done by calling `ShellExecute`
|
||||
with the `runas` verb. Since it's not trivial to read the standard output from an application
|
||||
launched in this way, a named pipe server is created at the user level, which waits for the response
|
||||
from `admin.exe` after it has been launched.
|
||||
|
||||
The name of the service executable and the data to be decrypted are passed via the command line to
|
||||
`admin.exe` like this:
|
||||
|
||||
```bat
|
||||
admin.exe --service-exe "c:\temp\service.exe" --encrypted "QVBQQgEAAADQjJ3fARXREYx6AMBPwpfrAQAAA..."
|
||||
```
|
||||
|
||||
**At this point, the user must permit the action to be performed on the UAC screen.**
|
||||
|
||||
## 2. Admin executable
|
||||
|
||||
This executable receives the full path of `service.exe` and the data to be decrypted.
|
||||
|
||||
First, it installs the service to run as SYSTEM and waits for it to start running. The service
|
||||
creates a named pipe server that the admin-level executable communicates with (see the `service.exe`
|
||||
description further down).
|
||||
|
||||
It sends the base64 string to the pipe server in a raw message and waits for the answer. The answer
|
||||
could be a success or a failure. In case of success, it's a base64 string decrypted at the system
|
||||
level. In case of failure, it's an error message prefixed with an `!`. In either case, the response
|
||||
is sent to the named pipe server created by the user. The user responds with `ok` (ignored).
|
||||
|
||||
After that, the executable stops and uninstalls the service and then exits.
|
||||
|
||||
## 3. System service
|
||||
|
||||
The service starts and creates a named pipe server for communication between `admin.exe` and the
|
||||
system service. Please note that it is not possible to communicate between the user and the system
|
||||
service directly via a named pipe. Thus, this three-layered approach is necessary.
|
||||
|
||||
Once the service is started, it waits for the incoming message via the named pipe. The expected
|
||||
message is a base64 string to be decrypted. The data is decrypted via the Windows Data Protection
|
||||
API `CryptUnprotectData` and sent back in response to this incoming message in base64 encoding. In
|
||||
case of an error, the error message is sent back prefixed with an `!`.
|
||||
|
||||
The service keeps running and servicing more requests if there are any, until it's stopped and
|
||||
removed from the system. Even though we send only one request, the service is designed to handle as
|
||||
many clients with as many messages as needed and could be installed on the system permanently if
|
||||
necessary.
|
||||
|
||||
## 4. Back to client library
|
||||
|
||||
The decrypted base64-encoded string comes back from the admin executable to the named pipe server at
|
||||
the user level. At this point, it has been decrypted only once at the system level.
|
||||
|
||||
In the next step, the string is decrypted at the user level with the same Windows Data Protection
|
||||
API.
|
||||
|
||||
And as the third step, it's decrypted with a hard-coded key found in the `elevation_service.exe`
|
||||
from the Chrome installation. Based on the version of the encrypted string (encoded in the string
|
||||
itself), it's either AES-256-GCM or ChaCha20Poly1305 encryption scheme. The details can be found in
|
||||
`windows.rs`.
|
||||
|
||||
After all of these steps, we have the master key which can be used to decrypt the password
|
||||
information stored in the local database.
|
||||
|
||||
## Summary
|
||||
|
||||
The Windows ABE decryption process involves a three-tier architecture with named pipe communication:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Client Library (User)
|
||||
participant Admin as admin.exe (Administrator)
|
||||
participant Service as service.exe (System)
|
||||
|
||||
Client->>Client: Create named pipe server
|
||||
Note over Client: \\.\pipe\BitwardenEncryptionService-admin-user
|
||||
|
||||
Client->>Admin: Launch with UAC elevation
|
||||
Note over Client,Admin: --service-exe c:\path\to\service.exe
|
||||
Note over Client,Admin: --encrypted QVBQQgEAAADQjJ3fARXRE...
|
||||
|
||||
Client->>Client: Wait for response
|
||||
|
||||
Admin->>Service: Install & start service
|
||||
Note over Admin,Service: c:\path\to\service.exe
|
||||
|
||||
Service->>Service: Create named pipe server
|
||||
Note over Service: \\.\pipe\BitwardenEncryptionService-service-admin
|
||||
|
||||
Service->>Service: Wait for message
|
||||
|
||||
Admin->>Service: Send encrypted data via admin-service pipe
|
||||
Note over Admin,Service: QVBQQgEAAADQjJ3fARXRE...
|
||||
|
||||
Admin->>Admin: Wait for response
|
||||
|
||||
Service->>Service: Decrypt with system-level DPAPI
|
||||
|
||||
Service->>Admin: Return decrypted data via admin-service pipe
|
||||
Note over Service,Admin: EjRWeXN0ZW0gU2VydmljZQ...
|
||||
|
||||
Admin->>Client: Send result via named user-admin pipe
|
||||
Note over Client,Admin: EjRWeXN0ZW0gU2VydmljZQ...
|
||||
|
||||
Client->>Admin: Send ACK to admin
|
||||
Note over Client,Admin: ok
|
||||
|
||||
Admin->>Service: Stop & uninstall service
|
||||
Service-->>Admin: Exit
|
||||
|
||||
Admin-->>Client: Exit
|
||||
|
||||
Client->>Client: Decrypt with user-level DPAPI
|
||||
|
||||
Client->>Client: Decrypt with hardcoded key
|
||||
Note over Client: AES-256-GCM or ChaCha20Poly1305
|
||||
|
||||
Client->>Client: Done
|
||||
```
|
||||
@@ -0,0 +1,350 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use hex::decode;
|
||||
use homedir::my_home;
|
||||
use rusqlite::{params, Connection};
|
||||
|
||||
// Platform-specific code
|
||||
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
mod platform;
|
||||
|
||||
//
|
||||
// Public API
|
||||
//
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProfileInfo {
|
||||
pub name: String,
|
||||
pub folder: String,
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub account_name: Option<String>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub account_email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Login {
|
||||
pub url: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LoginImportFailure {
|
||||
pub url: String,
|
||||
pub username: String,
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoginImportResult {
|
||||
Success(Login),
|
||||
Failure(LoginImportFailure),
|
||||
}
|
||||
|
||||
// TODO: Make thus async
|
||||
pub fn get_installed_browsers() -> Result<Vec<String>> {
|
||||
let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len());
|
||||
|
||||
for (browser, config) in SUPPORTED_BROWSER_MAP.iter() {
|
||||
let data_dir = get_browser_data_dir(config)?;
|
||||
if data_dir.exists() {
|
||||
browsers.push((*browser).to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(browsers)
|
||||
}
|
||||
|
||||
// TODO: Make thus async
|
||||
pub fn get_available_profiles(browser_name: &String) -> Result<Vec<ProfileInfo>> {
|
||||
let (_, local_state) = load_local_state_for_browser(browser_name)?;
|
||||
Ok(get_profile_info(&local_state))
|
||||
}
|
||||
|
||||
pub async fn import_logins(
|
||||
browser_name: &String,
|
||||
profile_id: &String,
|
||||
) -> Result<Vec<LoginImportResult>> {
|
||||
let (data_dir, local_state) = load_local_state_for_browser(browser_name)?;
|
||||
|
||||
let mut crypto_service = platform::get_crypto_service(browser_name, &local_state)
|
||||
.map_err(|e| anyhow!("Failed to get crypto service: {}", e))?;
|
||||
|
||||
let local_logins = get_logins(&data_dir, profile_id, "Login Data")
|
||||
.map_err(|e| anyhow!("Failed to query logins: {}", e))?;
|
||||
|
||||
// This is not available in all browsers, but there's no harm in trying. If the file doesn't exist we just get an empty vector.
|
||||
let account_logins = get_logins(&data_dir, profile_id, "Login Data For Account")
|
||||
.map_err(|e| anyhow!("Failed to query logins: {}", e))?;
|
||||
|
||||
// TODO: Do we need a better merge strategy? Maybe ignore duplicates at least?
|
||||
// TODO: Should we also ignore an error from one of the two imports? If one is successful and the other fails,
|
||||
// should we still return the successful ones? At the moment it doesn't fail for a missing file, only when
|
||||
// something goes really wrong.
|
||||
let all_logins = local_logins
|
||||
.into_iter()
|
||||
.chain(account_logins.into_iter())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let results = decrypt_logins(all_logins, &mut crypto_service).await;
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
//
|
||||
// Private
|
||||
//
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BrowserConfig {
|
||||
name: &'static str,
|
||||
data_dir: &'static str,
|
||||
}
|
||||
|
||||
static SUPPORTED_BROWSER_MAP: LazyLock<
|
||||
std::collections::HashMap<&'static str, &'static BrowserConfig>,
|
||||
> = LazyLock::new(|| {
|
||||
platform::SUPPORTED_BROWSERS
|
||||
.iter()
|
||||
.map(|b| (b.name, b))
|
||||
.collect::<std::collections::HashMap<_, _>>()
|
||||
});
|
||||
|
||||
fn get_browser_data_dir(config: &BrowserConfig) -> Result<PathBuf> {
|
||||
let dir = my_home()
|
||||
.map_err(|_| anyhow!("Home directory not found"))?
|
||||
.ok_or_else(|| anyhow!("Home directory not found"))?
|
||||
.join(config.data_dir);
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
//
|
||||
// CryptoService
|
||||
//
|
||||
|
||||
#[async_trait]
|
||||
trait CryptoService: Send {
|
||||
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String>;
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone)]
|
||||
struct LocalState {
|
||||
profile: AllProfiles,
|
||||
#[allow(dead_code)]
|
||||
os_crypt: Option<OsCrypt>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone)]
|
||||
struct AllProfiles {
|
||||
info_cache: std::collections::HashMap<String, OneProfile>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone)]
|
||||
struct OneProfile {
|
||||
name: String,
|
||||
gaia_name: Option<String>,
|
||||
user_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone)]
|
||||
struct OsCrypt {
|
||||
#[allow(dead_code)]
|
||||
encrypted_key: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
app_bound_encrypted_key: Option<String>,
|
||||
}
|
||||
|
||||
fn load_local_state_for_browser(browser_name: &String) -> Result<(PathBuf, LocalState)> {
|
||||
let config = SUPPORTED_BROWSER_MAP
|
||||
.get(browser_name.as_str())
|
||||
.ok_or_else(|| anyhow!("Unsupported browser: {}", browser_name))?;
|
||||
|
||||
let data_dir = get_browser_data_dir(config)?;
|
||||
if !data_dir.exists() {
|
||||
return Err(anyhow!(
|
||||
"Browser user data directory '{}' not found",
|
||||
data_dir.display()
|
||||
));
|
||||
}
|
||||
|
||||
let local_state = load_local_state(&data_dir)?;
|
||||
|
||||
Ok((data_dir, local_state))
|
||||
}
|
||||
|
||||
fn load_local_state(browser_dir: &Path) -> Result<LocalState> {
|
||||
let local_state = std::fs::read_to_string(browser_dir.join("Local State"))
|
||||
.map_err(|e| anyhow!("Failed to read local state file: {}", e))?;
|
||||
|
||||
serde_json::from_str(&local_state)
|
||||
.map_err(|e| anyhow!("Failed to parse local state JSON: {}", e))
|
||||
}
|
||||
|
||||
fn get_profile_info(local_state: &LocalState) -> Vec<ProfileInfo> {
|
||||
let mut profile_infos = Vec::new();
|
||||
for (name, info) in local_state.profile.info_cache.iter() {
|
||||
profile_infos.push(ProfileInfo {
|
||||
name: info.name.clone(),
|
||||
folder: name.clone(),
|
||||
account_name: info.gaia_name.clone(),
|
||||
account_email: info.user_name.clone(),
|
||||
});
|
||||
}
|
||||
profile_infos
|
||||
}
|
||||
|
||||
struct EncryptedLogin {
|
||||
url: String,
|
||||
username: String,
|
||||
encrypted_password: Vec<u8>,
|
||||
encrypted_note: Vec<u8>,
|
||||
}
|
||||
|
||||
fn get_logins(
|
||||
browser_dir: &Path,
|
||||
profile_id: &String,
|
||||
filename: &str,
|
||||
) -> Result<Vec<EncryptedLogin>> {
|
||||
let login_data_path = browser_dir.join(profile_id).join(filename);
|
||||
|
||||
// Sometimes database files are not present, so nothing to import
|
||||
if !login_data_path.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
// When the browser with the current profile is open the database file is locked.
|
||||
// To access it we need to copy it to a temporary location.
|
||||
let tmp_db_path = std::env::temp_dir().join(format!(
|
||||
"tmp-logins-{}-{}.db",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_err(|e| anyhow!("Failed to retrieve system time: {}", e))?
|
||||
.as_millis(),
|
||||
rand::random::<u32>()
|
||||
));
|
||||
|
||||
std::fs::copy(&login_data_path, &tmp_db_path).map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to copy the password database file at {:?}: {}",
|
||||
login_data_path,
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
let tmp_db_path = tmp_db_path
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Failed to locate database."))?;
|
||||
let maybe_logins =
|
||||
query_logins(tmp_db_path).map_err(|e| anyhow!("Failed to query logins: {}", e))?;
|
||||
|
||||
// Clean up temp file
|
||||
let _ = std::fs::remove_file(tmp_db_path);
|
||||
|
||||
Ok(maybe_logins)
|
||||
}
|
||||
|
||||
fn hex_to_bytes(hex: &str) -> Vec<u8> {
|
||||
decode(hex).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn does_table_exist(conn: &Connection, table_name: &str) -> Result<bool, rusqlite::Error> {
|
||||
let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?1")?;
|
||||
let exists = stmt.exists(params![table_name])?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
fn query_logins(db_path: &str) -> Result<Vec<EncryptedLogin>, rusqlite::Error> {
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
let have_logins = does_table_exist(&conn, "logins")?;
|
||||
let have_password_notes = does_table_exist(&conn, "password_notes")?;
|
||||
if !have_logins || !have_password_notes {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
r#"
|
||||
SELECT
|
||||
l.origin_url AS url,
|
||||
l.username_value AS username,
|
||||
hex(l.password_value) AS encryptedPasswordHex,
|
||||
hex(pn.value) AS encryptedNoteHex
|
||||
FROM
|
||||
logins l
|
||||
LEFT JOIN
|
||||
password_notes pn ON l.id = pn.parent_id
|
||||
WHERE
|
||||
l.blacklisted_by_user = 0
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let logins_iter = stmt.query_map((), |row| {
|
||||
let url: String = row.get("url")?;
|
||||
let username: String = row.get("username")?;
|
||||
let encrypted_password_hex: String = row.get("encryptedPasswordHex")?;
|
||||
let encrypted_note_hex: String = row.get("encryptedNoteHex")?;
|
||||
Ok(EncryptedLogin {
|
||||
url,
|
||||
username,
|
||||
encrypted_password: hex_to_bytes(&encrypted_password_hex),
|
||||
encrypted_note: hex_to_bytes(&encrypted_note_hex),
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut logins = Vec::new();
|
||||
for login in logins_iter {
|
||||
logins.push(login?);
|
||||
}
|
||||
|
||||
Ok(logins)
|
||||
}
|
||||
|
||||
async fn decrypt_logins(
|
||||
encrypted_logins: Vec<EncryptedLogin>,
|
||||
crypto_service: &mut Box<dyn CryptoService>,
|
||||
) -> Vec<LoginImportResult> {
|
||||
let mut results = Vec::with_capacity(encrypted_logins.len());
|
||||
for encrypted_login in encrypted_logins {
|
||||
let result = decrypt_login(encrypted_login, crypto_service).await;
|
||||
results.push(result);
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
async fn decrypt_login(
|
||||
encrypted_login: EncryptedLogin,
|
||||
crypto_service: &mut Box<dyn CryptoService>,
|
||||
) -> LoginImportResult {
|
||||
let maybe_password = crypto_service
|
||||
.decrypt_to_string(&encrypted_login.encrypted_password)
|
||||
.await;
|
||||
match maybe_password {
|
||||
Ok(password) => {
|
||||
let note = crypto_service
|
||||
.decrypt_to_string(&encrypted_login.encrypted_note)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
LoginImportResult::Success(Login {
|
||||
url: encrypted_login.url,
|
||||
username: encrypted_login.username,
|
||||
password,
|
||||
note,
|
||||
})
|
||||
}
|
||||
Err(e) => LoginImportResult::Failure(LoginImportFailure {
|
||||
url: encrypted_login.url,
|
||||
username: encrypted_login.username,
|
||||
error: e.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//! Cryptographic primitives used in the SDK
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
|
||||
use aes::cipher::{
|
||||
block_padding::Pkcs7, generic_array::GenericArray, typenum::U32, BlockDecryptMut, KeyIvInit,
|
||||
};
|
||||
|
||||
pub fn decrypt_aes256(iv: &[u8; 16], data: &[u8], key: GenericArray<u8, U32>) -> Result<Vec<u8>> {
|
||||
let iv = GenericArray::from_slice(iv);
|
||||
let mut data = data.to_vec();
|
||||
return cbc::Decryptor::<aes::Aes256>::new(&key, iv)
|
||||
.decrypt_padded_mut::<Pkcs7>(&mut data)
|
||||
.map_err(|_| anyhow!("Failed to decrypt data"))?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod chromium;
|
||||
@@ -0,0 +1,153 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use oo7::XDG_SCHEMA_ATTRIBUTE;
|
||||
|
||||
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
||||
|
||||
mod util;
|
||||
|
||||
//
|
||||
// Public API
|
||||
//
|
||||
|
||||
// TODO: It's possible that there might be multiple possible data directories, depending on the installation method (e.g., snap, flatpak, etc.).
|
||||
pub const SUPPORTED_BROWSERS: [BrowserConfig; 4] = [
|
||||
BrowserConfig {
|
||||
name: "Chrome",
|
||||
data_dir: ".config/google-chrome",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Chromium",
|
||||
data_dir: "snap/chromium/common/chromium",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Brave",
|
||||
data_dir: "snap/brave/current/.config/BraveSoftware/Brave-Browser",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Opera",
|
||||
data_dir: "snap/opera/current/.config/opera",
|
||||
},
|
||||
];
|
||||
|
||||
pub fn get_crypto_service(
|
||||
browser_name: &String,
|
||||
_local_state: &LocalState,
|
||||
) -> Result<Box<dyn CryptoService>> {
|
||||
let config = KEYRING_CONFIG
|
||||
.iter()
|
||||
.find(|b| b.browser == browser_name)
|
||||
.ok_or_else(|| anyhow!("Unsupported browser: {}", browser_name))?;
|
||||
let service = LinuxCryptoService::new(config);
|
||||
Ok(Box::new(service))
|
||||
}
|
||||
|
||||
//
|
||||
// Private
|
||||
//
|
||||
|
||||
#[derive(Debug)]
|
||||
struct KeyringConfig {
|
||||
browser: &'static str,
|
||||
application_id: &'static str,
|
||||
}
|
||||
|
||||
const KEYRING_CONFIG: [KeyringConfig; SUPPORTED_BROWSERS.len()] = [
|
||||
KeyringConfig {
|
||||
browser: "Chrome",
|
||||
application_id: "chrome",
|
||||
},
|
||||
KeyringConfig {
|
||||
browser: "Chromium",
|
||||
application_id: "chromium",
|
||||
},
|
||||
KeyringConfig {
|
||||
browser: "Brave",
|
||||
application_id: "brave",
|
||||
},
|
||||
KeyringConfig {
|
||||
browser: "Opera",
|
||||
application_id: "opera",
|
||||
},
|
||||
];
|
||||
|
||||
const IV: [u8; 16] = [0x20; 16];
|
||||
const V10_KEY: [u8; 16] = [
|
||||
0xfd, 0x62, 0x1f, 0xe5, 0xa2, 0xb4, 0x02, 0x53, 0x9d, 0xfa, 0x14, 0x7c, 0xa9, 0x27, 0x27, 0x78,
|
||||
];
|
||||
|
||||
struct LinuxCryptoService {
|
||||
config: &'static KeyringConfig,
|
||||
v11_key: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl LinuxCryptoService {
|
||||
fn new(config: &'static KeyringConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
v11_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt_v10(&self, encrypted: &[u8]) -> Result<String> {
|
||||
decrypt(&V10_KEY, encrypted)
|
||||
}
|
||||
|
||||
async fn decrypt_v11(&mut self, encrypted: &[u8]) -> Result<String> {
|
||||
if self.v11_key.is_none() {
|
||||
let master_password = get_master_password(self.config.application_id).await?;
|
||||
self.v11_key = Some(util::derive_saltysalt(&master_password, 1)?);
|
||||
}
|
||||
|
||||
let key = self
|
||||
.v11_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Failed to retrieve key"))?;
|
||||
decrypt(key, encrypted)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CryptoService for LinuxCryptoService {
|
||||
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String> {
|
||||
let (version, password) =
|
||||
util::split_encrypted_string_and_validate(encrypted, &["v10", "v11"])?;
|
||||
|
||||
let result = match version {
|
||||
"v10" => self.decrypt_v10(password),
|
||||
"v11" => self.decrypt_v11(password).await,
|
||||
_ => Err(anyhow!("Logic error: unreachable code")),
|
||||
}?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt(key: &[u8], encrypted: &[u8]) -> Result<String> {
|
||||
let plaintext = util::decrypt_aes_128_cbc(key, &IV, encrypted)?;
|
||||
String::from_utf8(plaintext).map_err(|e| anyhow!("UTF-8 error: {:?}", e))
|
||||
}
|
||||
|
||||
async fn get_master_password(application_tag: &str) -> Result<Vec<u8>> {
|
||||
let keyring = oo7::Keyring::new().await?;
|
||||
keyring.unlock().await?;
|
||||
|
||||
let attributes = HashMap::from([
|
||||
(
|
||||
XDG_SCHEMA_ATTRIBUTE,
|
||||
"chrome_libsecret_os_crypt_password_v2",
|
||||
),
|
||||
("application", application_tag),
|
||||
]);
|
||||
|
||||
let results = keyring.search_items(&attributes).await?;
|
||||
match results.first() {
|
||||
Some(r) => {
|
||||
let secret = r.secret().await?;
|
||||
Ok(secret.to_vec())
|
||||
}
|
||||
None => Err(anyhow!("The master password not found in the keyring")),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use security_framework::passwords::get_generic_password;
|
||||
|
||||
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
||||
|
||||
mod util;
|
||||
|
||||
//
|
||||
// Public API
|
||||
//
|
||||
|
||||
pub const SUPPORTED_BROWSERS: [BrowserConfig; 7] = [
|
||||
BrowserConfig {
|
||||
name: "Chrome",
|
||||
data_dir: "Library/Application Support/Google/Chrome",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Chromium",
|
||||
data_dir: "Library/Application Support/Chromium",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Microsoft Edge",
|
||||
data_dir: "Library/Application Support/Microsoft Edge",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Brave",
|
||||
data_dir: "Library/Application Support/BraveSoftware/Brave-Browser",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Arc",
|
||||
data_dir: "Library/Application Support/Arc/User Data",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Opera",
|
||||
data_dir: "Library/Application Support/com.operasoftware.Opera",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Vivaldi",
|
||||
data_dir: "Library/Application Support/Vivaldi",
|
||||
},
|
||||
];
|
||||
|
||||
pub fn get_crypto_service(
|
||||
browser_name: &String,
|
||||
_local_state: &LocalState,
|
||||
) -> Result<Box<dyn CryptoService>> {
|
||||
let config = KEYCHAIN_CONFIG
|
||||
.iter()
|
||||
.find(|b| b.browser == browser_name)
|
||||
.ok_or_else(|| anyhow!("Unsupported browser: {}", browser_name))?;
|
||||
|
||||
Ok(Box::new(MacCryptoService::new(config)))
|
||||
}
|
||||
|
||||
//
|
||||
// Private
|
||||
//
|
||||
|
||||
#[derive(Debug)]
|
||||
struct KeychainConfig {
|
||||
browser: &'static str,
|
||||
service: &'static str,
|
||||
account: &'static str,
|
||||
}
|
||||
|
||||
const KEYCHAIN_CONFIG: [KeychainConfig; SUPPORTED_BROWSERS.len()] = [
|
||||
KeychainConfig {
|
||||
browser: "Chrome",
|
||||
service: "Chrome Safe Storage",
|
||||
account: "Chrome",
|
||||
},
|
||||
KeychainConfig {
|
||||
browser: "Chromium",
|
||||
service: "Chromium Safe Storage",
|
||||
account: "Chromium",
|
||||
},
|
||||
KeychainConfig {
|
||||
browser: "Microsoft Edge",
|
||||
service: "Microsoft Edge Safe Storage",
|
||||
account: "Microsoft Edge",
|
||||
},
|
||||
KeychainConfig {
|
||||
browser: "Brave",
|
||||
service: "Brave Safe Storage",
|
||||
account: "Brave",
|
||||
},
|
||||
KeychainConfig {
|
||||
browser: "Arc",
|
||||
service: "Arc Safe Storage",
|
||||
account: "Arc",
|
||||
},
|
||||
KeychainConfig {
|
||||
browser: "Opera",
|
||||
service: "Opera Safe Storage",
|
||||
account: "Opera",
|
||||
},
|
||||
KeychainConfig {
|
||||
browser: "Vivaldi",
|
||||
service: "Vivaldi Safe Storage",
|
||||
account: "Vivaldi",
|
||||
},
|
||||
];
|
||||
|
||||
const IV: [u8; 16] = [0x20; 16]; // 16 bytes of 0x20 (space character)
|
||||
|
||||
//
|
||||
// CryptoService
|
||||
//
|
||||
|
||||
struct MacCryptoService {
|
||||
config: &'static KeychainConfig,
|
||||
master_key: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl MacCryptoService {
|
||||
fn new(config: &'static KeychainConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
master_key: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CryptoService for MacCryptoService {
|
||||
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String> {
|
||||
if encrypted.is_empty() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
// On macOS only v10 is supported
|
||||
let (_, no_prefix) = util::split_encrypted_string_and_validate(encrypted, &["v10"])?;
|
||||
|
||||
// This might bring up the admin password prompt
|
||||
if self.master_key.is_none() {
|
||||
self.master_key = Some(get_master_key(self.config.service, self.config.account)?);
|
||||
}
|
||||
|
||||
let key = self
|
||||
.master_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Failed to retrieve key"))?;
|
||||
let plaintext = util::decrypt_aes_128_cbc(key, &IV, no_prefix)
|
||||
.map_err(|e| anyhow!("Failed to decrypt: {}", e))?;
|
||||
let plaintext =
|
||||
String::from_utf8(plaintext).map_err(|e| anyhow!("Invalid UTF-8: {}", e))?;
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_master_key(service: &str, account: &str) -> Result<Vec<u8>> {
|
||||
let master_password = get_master_password(service, account)?;
|
||||
let key = util::derive_saltysalt(&master_password, 1003)?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
fn get_master_password(service: &str, account: &str) -> Result<Vec<u8>> {
|
||||
let password = get_generic_password(service, account)
|
||||
.map_err(|e| anyhow!("Failed to get password from keychain: {}", e))?;
|
||||
|
||||
Ok(password)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
|
||||
use anyhow::{anyhow, Result};
|
||||
use pbkdf2::{hmac::Hmac, pbkdf2};
|
||||
use sha1::Sha1;
|
||||
|
||||
pub fn split_encrypted_string(encrypted: &[u8]) -> Result<(&str, &[u8])> {
|
||||
if encrypted.len() < 3 {
|
||||
return Err(anyhow!(
|
||||
"Corrupted entry: invalid encrypted string length, expected at least 3 bytes, got {}",
|
||||
encrypted.len()
|
||||
));
|
||||
}
|
||||
|
||||
let (version, password) = encrypted.split_at(3);
|
||||
Ok((std::str::from_utf8(version)?, password))
|
||||
}
|
||||
|
||||
pub fn split_encrypted_string_and_validate<'a>(
|
||||
encrypted: &'a [u8],
|
||||
supported_versions: &[&str],
|
||||
) -> Result<(&'a str, &'a [u8])> {
|
||||
let (version, password) = split_encrypted_string(encrypted)?;
|
||||
if !supported_versions.contains(&version) {
|
||||
return Err(anyhow!("Unsupported encryption version: {}", version));
|
||||
}
|
||||
|
||||
Ok((version, password))
|
||||
}
|
||||
|
||||
pub fn decrypt_aes_128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
|
||||
let decryptor = cbc::Decryptor::<aes::Aes128>::new_from_slices(key, iv)?;
|
||||
let plaintext = decryptor
|
||||
.decrypt_padded_vec_mut::<Pkcs7>(ciphertext)
|
||||
.map_err(|e| anyhow!("Failed to decrypt: {}", e))?;
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
pub fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> {
|
||||
let mut key = vec![0u8; 16];
|
||||
pbkdf2::<Hmac<Sha1>>(password, b"saltysalt", iterations, &mut key)
|
||||
.map_err(|e| anyhow!("Failed to derive master key: {}", e))?;
|
||||
Ok(key)
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
use aes_gcm::aead::Aead;
|
||||
use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
||||
use winapi::shared::minwindef::{BOOL, BYTE, DWORD};
|
||||
use winapi::um::{dpapi::CryptUnprotectData, wincrypt::DATA_BLOB};
|
||||
use windows::Win32::Foundation::{LocalFree, HLOCAL};
|
||||
|
||||
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
//
|
||||
// Public API
|
||||
//
|
||||
|
||||
pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [
|
||||
BrowserConfig {
|
||||
name: "Chrome",
|
||||
data_dir: "AppData/Local/Google/Chrome/User Data",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Chromium",
|
||||
data_dir: "AppData/Local/Chromium/User Data",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Microsoft Edge",
|
||||
data_dir: "AppData/Local/Microsoft/Edge/User Data",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Brave",
|
||||
data_dir: "AppData/Local/BraveSoftware/Brave-Browser/User Data",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Opera",
|
||||
data_dir: "AppData/Roaming/Opera Software/Opera Stable",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Vivaldi",
|
||||
data_dir: "AppData/Local/Vivaldi/User Data",
|
||||
},
|
||||
];
|
||||
|
||||
pub fn get_crypto_service(
|
||||
_browser_name: &str,
|
||||
local_state: &LocalState,
|
||||
) -> Result<Box<dyn CryptoService>> {
|
||||
Ok(Box::new(WindowsCryptoService::new(local_state)))
|
||||
}
|
||||
|
||||
//
|
||||
// CryptoService
|
||||
//
|
||||
struct WindowsCryptoService {
|
||||
master_key: Option<Vec<u8>>,
|
||||
encrypted_key: Option<String>,
|
||||
}
|
||||
|
||||
impl WindowsCryptoService {
|
||||
pub(crate) fn new(local_state: &LocalState) -> Self {
|
||||
Self {
|
||||
master_key: None,
|
||||
encrypted_key: local_state
|
||||
.os_crypt
|
||||
.as_ref()
|
||||
.and_then(|c| c.encrypted_key.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CryptoService for WindowsCryptoService {
|
||||
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String> {
|
||||
if encrypted.is_empty() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
// On Windows only v10 and v20 are supported at the moment
|
||||
let (version, no_prefix) =
|
||||
util::split_encrypted_string_and_validate(encrypted, &["v10", "v20"])?;
|
||||
|
||||
// v10 is already stripped; Windows Chrome uses AES-GCM: [12 bytes IV][ciphertext][16 bytes auth tag]
|
||||
const IV_SIZE: usize = 12;
|
||||
const TAG_SIZE: usize = 16;
|
||||
const MIN_LENGTH: usize = IV_SIZE + TAG_SIZE;
|
||||
|
||||
if no_prefix.len() < MIN_LENGTH {
|
||||
return Err(anyhow!(
|
||||
"Corrupted entry: expected at least {} bytes, got {} bytes",
|
||||
MIN_LENGTH,
|
||||
no_prefix.len()
|
||||
));
|
||||
}
|
||||
|
||||
// Allow empty passwords
|
||||
if no_prefix.len() == MIN_LENGTH {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
if self.master_key.is_none() {
|
||||
self.master_key = Some(self.get_master_key(version)?);
|
||||
}
|
||||
|
||||
let key = self
|
||||
.master_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Failed to retrieve key"))?;
|
||||
let key = Key::<Aes256Gcm>::from_slice(key);
|
||||
let cipher = Aes256Gcm::new(key);
|
||||
let nonce = Nonce::from_slice(&no_prefix[..IV_SIZE]);
|
||||
|
||||
let decrypted_bytes = cipher
|
||||
.decrypt(nonce, no_prefix[IV_SIZE..].as_ref())
|
||||
.map_err(|e| anyhow!("Decryption failed: {}", e))?;
|
||||
|
||||
let plaintext = String::from_utf8(decrypted_bytes)
|
||||
.map_err(|e| anyhow!("Failed to convert decrypted data to UTF-8: {}", e))?;
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowsCryptoService {
|
||||
fn get_master_key(&mut self, version: &str) -> Result<Vec<u8>> {
|
||||
match version {
|
||||
"v10" => self.get_master_key_v10(),
|
||||
_ => Err(anyhow!("Unsupported version: {}", version)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_master_key_v10(&mut self) -> Result<Vec<u8>> {
|
||||
if self.encrypted_key.is_none() {
|
||||
return Err(anyhow!(
|
||||
"Encrypted master key is not found in the local browser state"
|
||||
));
|
||||
}
|
||||
|
||||
let key = self
|
||||
.encrypted_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Failed to retrieve key"))?;
|
||||
let key_bytes = BASE64_STANDARD
|
||||
.decode(key)
|
||||
.map_err(|e| anyhow!("Encrypted master key is not a valid base64 string: {}", e))?;
|
||||
|
||||
if key_bytes.len() <= 5 || &key_bytes[..5] != b"DPAPI" {
|
||||
return Err(anyhow!("Encrypted master key is not encrypted with DPAPI"));
|
||||
}
|
||||
|
||||
let key = unprotect_data_win(&key_bytes[5..])
|
||||
.map_err(|e| anyhow!("Failed to unprotect the master key: {}", e))?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
}
|
||||
|
||||
fn unprotect_data_win(data: &[u8]) -> Result<Vec<u8>> {
|
||||
if data.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut data_in = DATA_BLOB {
|
||||
cbData: data.len() as DWORD,
|
||||
pbData: data.as_ptr() as *mut BYTE,
|
||||
};
|
||||
|
||||
let mut data_out = DATA_BLOB {
|
||||
cbData: 0,
|
||||
pbData: std::ptr::null_mut(),
|
||||
};
|
||||
|
||||
let result: BOOL = unsafe {
|
||||
// BOOL from winapi (i32)
|
||||
CryptUnprotectData(
|
||||
&mut data_in,
|
||||
std::ptr::null_mut(), // ppszDataDescr: *mut LPWSTR (*mut *mut u16)
|
||||
std::ptr::null_mut(), // pOptionalEntropy: *mut DATA_BLOB
|
||||
std::ptr::null_mut(), // pvReserved: LPVOID (*mut c_void)
|
||||
std::ptr::null_mut(), // pPromptStruct: *mut CRYPTPROTECT_PROMPTSTRUCT
|
||||
0, // dwFlags: DWORD
|
||||
&mut data_out,
|
||||
)
|
||||
};
|
||||
|
||||
if result == 0 {
|
||||
return Err(anyhow!("CryptUnprotectData failed"));
|
||||
}
|
||||
|
||||
if data_out.pbData.is_null() || data_out.cbData == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let output_slice =
|
||||
unsafe { std::slice::from_raw_parts(data_out.pbData, data_out.cbData as usize) };
|
||||
|
||||
unsafe {
|
||||
if !data_out.pbData.is_null() {
|
||||
LocalFree(Some(HLOCAL(data_out.pbData as *mut std::ffi::c_void)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output_slice.to_vec())
|
||||
}
|
||||
@@ -35,7 +35,7 @@ function buildProxyBin(target, release = true) {
|
||||
const targetArg = target ? `--target ${target}` : "";
|
||||
const releaseArg = release ? "--release" : "";
|
||||
child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")});
|
||||
|
||||
|
||||
if (target) {
|
||||
// Copy the resulting binary to the dist folder
|
||||
const targetFolder = release ? "release" : "debug";
|
||||
|
||||
@@ -39,6 +39,7 @@ rand = { workspace = true }
|
||||
rsa = { workspace = true }
|
||||
russh-cryptovec = { workspace = true }
|
||||
scopeguard = { workspace = true }
|
||||
secmem-proc = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
ssh-encoding = { workspace = true }
|
||||
ssh-key = { workspace = true, features = [
|
||||
|
||||
@@ -20,6 +20,8 @@ pub fn disable_coredumps() -> Result<()> {
|
||||
rlim_cur: 0,
|
||||
rlim_max: 0,
|
||||
};
|
||||
println!("[Process Isolation] Disabling core dumps via setrlimit");
|
||||
|
||||
if unsafe { libc::setrlimit(RLIMIT_CORE, &rlimit) } != 0 {
|
||||
let e = std::io::Error::last_os_error();
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -44,11 +46,17 @@ pub fn is_core_dumping_disabled() -> Result<bool> {
|
||||
Ok(rlimit.rlim_cur == 0 && rlimit.rlim_max == 0)
|
||||
}
|
||||
|
||||
pub fn disable_memory_access() -> Result<()> {
|
||||
pub fn isolate_process() -> Result<()> {
|
||||
let pid = std::process::id();
|
||||
println!(
|
||||
"[Process Isolation] Disabling ptrace and memory access for main ({}) via PR_SET_DUMPABLE",
|
||||
pid
|
||||
);
|
||||
|
||||
if unsafe { libc::prctl(PR_SET_DUMPABLE, 0) } != 0 {
|
||||
let e = std::io::Error::last_os_error();
|
||||
return Err(anyhow::anyhow!(
|
||||
"failed to disable memory dumping, memory is dumpable by other processes {}",
|
||||
"failed to disable memory dumping, memory may be accessible by other processes {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
|
||||
@@ -8,6 +8,17 @@ pub fn is_core_dumping_disabled() -> Result<bool> {
|
||||
bail!("Not implemented on Mac")
|
||||
}
|
||||
|
||||
pub fn disable_memory_access() -> Result<()> {
|
||||
bail!("Not implemented on Mac")
|
||||
pub fn isolate_process() -> Result<()> {
|
||||
let pid: u32 = std::process::id();
|
||||
println!(
|
||||
"[Process Isolation] Disabling ptrace on main process ({}) via PT_DENY_ATTACH",
|
||||
pid
|
||||
);
|
||||
|
||||
secmem_proc::harden_process().map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"failed to disable memory dumping, memory may be accessible by other processes {}",
|
||||
e
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
//! This module implements process isolation, which aims to protect
|
||||
//! a process from dumping memory to disk when crashing, and from
|
||||
//! userspace memory access.
|
||||
//!
|
||||
//! On Windows, by default most userspace apps can read the memory of all
|
||||
//! other apps, and attach debuggers. On Mac, this is not possible, and only
|
||||
//! after granting developer permissions can an app attach to processes via
|
||||
//! ptrace / read memory. On Linux, this depends on the distro / configuration of yama
|
||||
//! `https://linux-audit.com/protect-ptrace-processes-kernel-yama-ptrace_scope/`
|
||||
//! For instance, ubuntu prevents ptrace of other processes by default.
|
||||
//! On Fedora, there are change proposals but ptracing is still possible unless
|
||||
//! otherwise configured.
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
|
||||
@@ -8,6 +8,17 @@ pub fn is_core_dumping_disabled() -> Result<bool> {
|
||||
bail!("Not implemented on Windows")
|
||||
}
|
||||
|
||||
pub fn disable_memory_access() -> Result<()> {
|
||||
bail!("Not implemented on Windows")
|
||||
pub fn isolate_process() -> Result<()> {
|
||||
let pid: u32 = std::process::id();
|
||||
println!(
|
||||
"[Process Isolation] Isolating main process via DACL {}",
|
||||
pid
|
||||
);
|
||||
|
||||
secmem_proc::harden_process().map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"failed to isolate process, memory may be accessible by other processes {}",
|
||||
e
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ manual_test = []
|
||||
anyhow = { workspace = true }
|
||||
autotype = { path = "../autotype" }
|
||||
base64 = { workspace = true }
|
||||
bitwarden_chromium_importer = { path = "../bitwarden_chromium_importer" }
|
||||
desktop_core = { path = "../core" }
|
||||
hex = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
26
apps/desktop/desktop_native/napi/index.d.ts
vendored
26
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -82,7 +82,7 @@ export declare namespace sshagent {
|
||||
export declare namespace processisolations {
|
||||
export function disableCoredumps(): Promise<void>
|
||||
export function isCoreDumpingDisabled(): Promise<boolean>
|
||||
export function disableMemoryAccess(): Promise<void>
|
||||
export function isolateProcess(): Promise<void>
|
||||
}
|
||||
export declare namespace powermonitors {
|
||||
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
||||
@@ -208,6 +208,30 @@ export declare namespace logging {
|
||||
}
|
||||
export function initNapiLog(jsLogFn: (err: Error | null, arg0: LogLevel, arg1: string) => any): void
|
||||
}
|
||||
export declare namespace chromium_importer {
|
||||
export interface ProfileInfo {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
export interface Login {
|
||||
url: string
|
||||
username: string
|
||||
password: string
|
||||
note: string
|
||||
}
|
||||
export interface LoginImportFailure {
|
||||
url: string
|
||||
username: string
|
||||
error: string
|
||||
}
|
||||
export interface LoginImportResult {
|
||||
login?: Login
|
||||
failure?: LoginImportFailure
|
||||
}
|
||||
export function getInstalledBrowsers(): Promise<Array<string>>
|
||||
export function getAvailableProfiles(browser: string): Promise<Array<ProfileInfo>>
|
||||
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
|
||||
}
|
||||
export declare namespace autotype {
|
||||
export function getForegroundWindowTitle(): string
|
||||
export function typeInput(input: Array<number>): void
|
||||
|
||||
@@ -336,8 +336,8 @@ pub mod processisolations {
|
||||
|
||||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||||
#[napi]
|
||||
pub async fn disable_memory_access() -> napi::Result<()> {
|
||||
desktop_core::process_isolation::disable_memory_access()
|
||||
pub async fn isolate_process() -> napi::Result<()> {
|
||||
desktop_core::process_isolation::isolate_process()
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -878,6 +878,96 @@ pub mod logging {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub mod chromium_importer {
|
||||
use bitwarden_chromium_importer::chromium::LoginImportResult as _LoginImportResult;
|
||||
use bitwarden_chromium_importer::chromium::ProfileInfo as _ProfileInfo;
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ProfileInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct Login {
|
||||
pub url: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct LoginImportFailure {
|
||||
pub url: String,
|
||||
pub username: String,
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct LoginImportResult {
|
||||
pub login: Option<Login>,
|
||||
pub failure: Option<LoginImportFailure>,
|
||||
}
|
||||
|
||||
impl From<_LoginImportResult> for LoginImportResult {
|
||||
fn from(l: _LoginImportResult) -> Self {
|
||||
match l {
|
||||
_LoginImportResult::Success(l) => LoginImportResult {
|
||||
login: Some(Login {
|
||||
url: l.url,
|
||||
username: l.username,
|
||||
password: l.password,
|
||||
note: l.note,
|
||||
}),
|
||||
failure: None,
|
||||
},
|
||||
_LoginImportResult::Failure(l) => LoginImportResult {
|
||||
login: None,
|
||||
failure: Some(LoginImportFailure {
|
||||
url: l.url,
|
||||
username: l.username,
|
||||
error: l.error,
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<_ProfileInfo> for ProfileInfo {
|
||||
fn from(p: _ProfileInfo) -> Self {
|
||||
ProfileInfo {
|
||||
id: p.folder,
|
||||
name: p.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_installed_browsers() -> napi::Result<Vec<String>> {
|
||||
bitwarden_chromium_importer::chromium::get_installed_browsers()
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_available_profiles(browser: String) -> napi::Result<Vec<ProfileInfo>> {
|
||||
bitwarden_chromium_importer::chromium::get_available_profiles(&browser)
|
||||
.map(|profiles| profiles.into_iter().map(ProfileInfo::from).collect())
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn import_logins(
|
||||
browser: String,
|
||||
profile_id: String,
|
||||
) -> napi::Result<Vec<LoginImportResult>> {
|
||||
bitwarden_chromium_importer::chromium::import_logins(&browser, &profile_id)
|
||||
.await
|
||||
.map(|logins| logins.into_iter().map(LoginImportResult::from).collect())
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub mod autotype {
|
||||
#[napi]
|
||||
|
||||
@@ -330,6 +330,33 @@
|
||||
"enableBrowserIntegrationFingerprintDesc" | i18n
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="showEnableAutotype">
|
||||
<div class="checkbox">
|
||||
<label for="enableAutotype">
|
||||
<input
|
||||
id="enableAutotype"
|
||||
type="checkbox"
|
||||
formControlName="enableAutotype"
|
||||
(change)="saveEnableAutotype()"
|
||||
/>
|
||||
{{ "enableAutotypeTransitionKey" | i18n }}
|
||||
<button
|
||||
type="button"
|
||||
bitBadge
|
||||
variant="success"
|
||||
(click)="openPremiumDialog()"
|
||||
*ngIf="!hasPremium"
|
||||
>
|
||||
{{ "premium" | i18n }}
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
<small class="help-block" *ngIf="form.value.enableAutotype">
|
||||
<b>{{ "important" | i18n }}</b>
|
||||
{{ "enableAutotypeDescriptionTransitionKey" | i18n }}
|
||||
<b>{{ "editShortcut" | i18n }}</b></small
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label for="enableHardwareAcceleration">
|
||||
@@ -413,22 +440,6 @@
|
||||
"enableDuckDuckGoBrowserIntegrationDesc" | i18n
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="showEnableAutotype">
|
||||
<div class="checkbox">
|
||||
<label for="enableAutotype">
|
||||
<input
|
||||
id="enableAutotype"
|
||||
type="checkbox"
|
||||
formControlName="enableAutotype"
|
||||
(change)="saveEnableAutotype()"
|
||||
/>
|
||||
{{ "enableAutotype" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<small class="help-block"
|
||||
><b>{{ "important" | i18n }}</b> {{ "enableAutotypeDescription" | i18n }}</small
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="theme">{{ "theme" | i18n }}</label>
|
||||
<select
|
||||
|
||||
@@ -11,6 +11,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import {
|
||||
@@ -70,6 +71,8 @@ describe("SettingsComponent", () => {
|
||||
const keyService = mock<KeyService>();
|
||||
const dialogService = mock<DialogService>();
|
||||
const desktopAutotypeService = mock<DesktopAutotypeService>();
|
||||
const billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
const configService = mock<ConfigService>();
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
@@ -99,7 +102,7 @@ describe("SettingsComponent", () => {
|
||||
},
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: BiometricStateService, useValue: biometricStateService },
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{
|
||||
provide: DesktopAutofillSettingsService,
|
||||
useValue: desktopAutofillSettingsService,
|
||||
@@ -127,6 +130,7 @@ describe("SettingsComponent", () => {
|
||||
{ provide: MessagingService, useValue: messagingService },
|
||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||
{ provide: DesktopAutotypeService, useValue: desktopAutotypeService },
|
||||
{ provide: BillingAccountProfileStateService, useValue: billingAccountProfileStateService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
@@ -177,7 +181,9 @@ describe("SettingsComponent", () => {
|
||||
i18nService.userSetLocale$ = of("en");
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(false);
|
||||
policyService.policiesByType$.mockReturnValue(of([null]));
|
||||
desktopAutotypeService.autotypeEnabled$ = of(false);
|
||||
desktopAutotypeService.resolvedAutotypeEnabled$ = of(false);
|
||||
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -638,4 +644,27 @@ describe("SettingsComponent", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("desktop autotype", () => {
|
||||
it("autotype should be hidden on mac os", async () => {
|
||||
// Set OS
|
||||
platformUtilsService.getDevice.mockReturnValue(DeviceType.MacOsDesktop);
|
||||
|
||||
// Recreate component to apply the correct device
|
||||
fixture = TestBed.createComponent(SettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
// `enableAutotype` label shouldn't be found
|
||||
const showEnableAutotypeLabelElement = fixture.debugElement.query(
|
||||
By.css("label[for='enableAutotype']"),
|
||||
);
|
||||
expect(showEnableAutotypeLabelElement).toBeNull();
|
||||
|
||||
// `showEnableAutotype` should be false
|
||||
expect(component.showEnableAutotype).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from "@
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
@@ -50,6 +51,7 @@ import {
|
||||
SelectModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
BadgeComponent,
|
||||
} from "@bitwarden/components";
|
||||
import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management";
|
||||
import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";
|
||||
@@ -58,6 +60,7 @@ import { SetPinComponent } from "../../auth/components/set-pin.component";
|
||||
import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting";
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopAutotypeService } from "../../autofill/services/desktop-autotype.service";
|
||||
import { PremiumComponent } from "../../billing/app/accounts/premium.component";
|
||||
import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
import { NativeMessagingManifestService } from "../services/native-messaging-manifest.service";
|
||||
@@ -67,6 +70,7 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man
|
||||
templateUrl: "settings.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
BadgeComponent,
|
||||
CheckboxModule,
|
||||
CommonModule,
|
||||
FormFieldModule,
|
||||
@@ -130,6 +134,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
pinEnabled$: Observable<boolean> = of(true);
|
||||
|
||||
hasPremium: boolean = false;
|
||||
|
||||
form = this.formBuilder.group({
|
||||
// Security
|
||||
vaultTimeout: [null as VaultTimeout | null],
|
||||
@@ -158,7 +164,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
sshAgentPromptBehavior: SshAgentPromptType.Always,
|
||||
allowScreenshots: false,
|
||||
enableDuckDuckGoBrowserIntegration: false,
|
||||
enableAutotype: false,
|
||||
enableAutotype: this.formBuilder.control<boolean>({
|
||||
value: false,
|
||||
disabled: true,
|
||||
}),
|
||||
theme: [null as Theme | null],
|
||||
locale: [null as string | null],
|
||||
});
|
||||
@@ -193,6 +202,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
private validationService: ValidationService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private toastService: ToastService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {
|
||||
this.isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
|
||||
this.isLinux = this.platformUtilsService.getDevice() === DeviceType.LinuxDesktop;
|
||||
@@ -268,10 +278,14 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Autotype is for Windows initially
|
||||
const isWindows = this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop;
|
||||
const windowsDesktopAutotypeFeatureFlag = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.WindowsDesktopAutotype,
|
||||
);
|
||||
this.showEnableAutotype = isWindows && windowsDesktopAutotypeFeatureFlag;
|
||||
if (isWindows) {
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enabled) => {
|
||||
this.showEnableAutotype = enabled;
|
||||
});
|
||||
}
|
||||
|
||||
this.userHasMasterPassword = await this.userVerificationService.hasMasterPassword();
|
||||
|
||||
@@ -377,7 +391,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
this.desktopSettingsService.sshAgentPromptBehavior$,
|
||||
),
|
||||
allowScreenshots: !(await firstValueFrom(this.desktopSettingsService.preventScreenshots$)),
|
||||
enableAutotype: await firstValueFrom(this.desktopAutotypeService.autotypeEnabled$),
|
||||
enableAutotype: await firstValueFrom(this.desktopAutotypeService.resolvedAutotypeEnabled$),
|
||||
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
|
||||
locale: await firstValueFrom(this.i18nService.userSetLocale$),
|
||||
};
|
||||
@@ -402,6 +416,19 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
this.form.controls.vaultTimeoutAction.setValue(action, { emitEvent: false });
|
||||
});
|
||||
|
||||
if (isWindows) {
|
||||
this.billingAccountProfileStateService
|
||||
.hasPremiumFromAnySource$(activeAccount.id)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((hasPremium) => {
|
||||
this.hasPremium = hasPremium;
|
||||
|
||||
if (this.hasPremium) {
|
||||
this.form.controls.enableAutotype.enable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Form events
|
||||
this.form.controls.vaultTimeout.valueChanges
|
||||
.pipe(
|
||||
@@ -865,6 +892,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async openPremiumDialog() {
|
||||
await this.dialogService.open(PremiumComponent);
|
||||
}
|
||||
|
||||
async saveEnableAutotype() {
|
||||
await this.desktopAutotypeService.setAutotypeEnabledState(this.form.value.enableAutotype);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
NewDeviceVerificationComponent,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
@@ -296,6 +296,16 @@ const routes: Routes = [
|
||||
component: ChangePasswordComponent,
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
{
|
||||
path: "confirm-key-connector-domain",
|
||||
component: ConfirmKeyConnectorDomainComponent,
|
||||
canActivate: [],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "confirmKeyConnectorDomain",
|
||||
},
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
@@ -462,6 +463,7 @@ const safeProviders: SafeProvider[] = [
|
||||
ConfigService,
|
||||
GlobalStateProvider,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
BillingAccountProfileStateService,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import { chromium_importer } from "@bitwarden/desktop-napi";
|
||||
|
||||
export class ChromiumImporterService {
|
||||
constructor() {
|
||||
ipcMain.handle("chromium_importer.getInstalledBrowsers", async (event) => {
|
||||
return await chromium_importer.getInstalledBrowsers();
|
||||
});
|
||||
|
||||
ipcMain.handle("chromium_importer.getAvailableProfiles", async (event, browser: string) => {
|
||||
return await chromium_importer.getAvailableProfiles(browser);
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
"chromium_importer.importLogins",
|
||||
async (event, browser: string, profileId: string) => {
|
||||
return await chromium_importer.importLogins(browser, profileId);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
(formLoading)="this.loading = $event"
|
||||
(formDisabled)="this.disabled = $event"
|
||||
(onSuccessfulImport)="this.onSuccessfulImport($event)"
|
||||
[onImportFromBrowser]="this.onImportFromBrowser"
|
||||
[onLoadProfilesFromBrowser]="this.onLoadProfilesFromBrowser"
|
||||
></tools-import>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
|
||||
@@ -28,4 +28,12 @@ export class ImportDesktopComponent {
|
||||
protected async onSuccessfulImport(organizationId: string): Promise<void> {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
protected onLoadProfilesFromBrowser(browser: string): Promise<any[]> {
|
||||
return ipc.tools.chromiumImporter.getAvailableProfiles(browser);
|
||||
}
|
||||
|
||||
protected onImportFromBrowser(browser: string, profile: string): Promise<any[]> {
|
||||
return ipc.tools.chromiumImporter.importLogins(browser, profile);
|
||||
}
|
||||
}
|
||||
|
||||
14
apps/desktop/src/app/tools/preload.ts
Normal file
14
apps/desktop/src/app/tools/preload.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
const chromiumImporter = {
|
||||
getInstalledBrowsers: (): Promise<string[]> =>
|
||||
ipcRenderer.invoke("chromium_importer.getInstalledBrowsers"),
|
||||
getAvailableProfiles: (browser: string): Promise<any[]> =>
|
||||
ipcRenderer.invoke("chromium_importer.getAvailableProfiles", browser),
|
||||
importLogins: (browser: string, profileId: string): Promise<any[]> =>
|
||||
ipcRenderer.invoke("chromium_importer.importLogins", browser, profileId),
|
||||
};
|
||||
|
||||
export default {
|
||||
chromiumImporter,
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { combineLatest, filter, firstValueFrom, map, Observable, of, switchMap }
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -25,7 +26,7 @@ export const AUTOTYPE_ENABLED = new KeyDefinition<boolean>(
|
||||
export class DesktopAutotypeService {
|
||||
private readonly autotypeEnabledState = this.globalStateProvider.get(AUTOTYPE_ENABLED);
|
||||
|
||||
autotypeEnabled$: Observable<boolean> = of(false);
|
||||
resolvedAutotypeEnabled$: Observable<boolean> = of(false);
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
@@ -34,6 +35,7 @@ export class DesktopAutotypeService {
|
||||
private configService: ConfigService,
|
||||
private globalStateProvider: GlobalStateProvider,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {
|
||||
ipc.autofill.listenAutotypeRequest(async (windowTitle, callback) => {
|
||||
const possibleCiphers = await this.matchCiphersToWindowTitle(windowTitle);
|
||||
@@ -48,23 +50,30 @@ export class DesktopAutotypeService {
|
||||
|
||||
async init() {
|
||||
if (this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop) {
|
||||
this.autotypeEnabled$ = combineLatest([
|
||||
this.resolvedAutotypeEnabled$ = combineLatest([
|
||||
this.autotypeEnabledState.state$,
|
||||
this.configService.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype),
|
||||
this.accountService.activeAccount$.pipe(
|
||||
map((account) => account?.id),
|
||||
map((activeAccount) => activeAccount?.id),
|
||||
switchMap((userId) => this.authService.authStatusFor$(userId)),
|
||||
),
|
||||
this.accountService.activeAccount$.pipe(
|
||||
map((activeAccount) => activeAccount?.id),
|
||||
switchMap((userId) =>
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId),
|
||||
),
|
||||
),
|
||||
]).pipe(
|
||||
map(
|
||||
([autotypeEnabled, windowsDesktopAutotypeFeatureFlag, authStatus]) =>
|
||||
([autotypeEnabled, windowsDesktopAutotypeFeatureFlag, authStatus, hasPremium]) =>
|
||||
autotypeEnabled &&
|
||||
windowsDesktopAutotypeFeatureFlag &&
|
||||
authStatus == AuthenticationStatus.Unlocked,
|
||||
authStatus == AuthenticationStatus.Unlocked &&
|
||||
hasPremium,
|
||||
),
|
||||
);
|
||||
|
||||
this.autotypeEnabled$.subscribe((enabled) => {
|
||||
this.resolvedAutotypeEnabled$.subscribe((enabled) => {
|
||||
ipc.autofill.configureAutotype(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,10 +153,7 @@ export class SshAgentService implements OnDestroy {
|
||||
|
||||
if (isListRequest) {
|
||||
const sshCiphers = ciphers.filter(
|
||||
(cipher) =>
|
||||
cipher.type === CipherType.SshKey &&
|
||||
!cipher.isDeleted &&
|
||||
cipher.organizationId == null,
|
||||
(cipher) => cipher.type === CipherType.SshKey && !cipher.isDeleted,
|
||||
);
|
||||
const keys = sshCiphers.map((cipher) => {
|
||||
return {
|
||||
@@ -266,10 +263,7 @@ export class SshAgentService implements OnDestroy {
|
||||
}
|
||||
|
||||
const sshCiphers = ciphers.filter(
|
||||
(cipher) =>
|
||||
cipher.type === CipherType.SshKey &&
|
||||
!cipher.isDeleted &&
|
||||
cipher.organizationId == null,
|
||||
(cipher) => cipher.type === CipherType.SshKey && !cipher.isDeleted,
|
||||
);
|
||||
const keys = sshCiphers.map((cipher) => {
|
||||
return {
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
>
|
||||
<b>{{ "premiumPurchase" | i18n }}</b>
|
||||
</button>
|
||||
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
|
||||
<button type="button" bitDialogClose>{{ "close" | i18n }}</button>
|
||||
<div class="right" *ngIf="!(isPremium$ | async)">
|
||||
<button
|
||||
#refreshBtn
|
||||
|
||||
@@ -3588,6 +3588,12 @@
|
||||
"awaitingSSODesc": {
|
||||
"message": "Please continue to log in using your company credentials."
|
||||
},
|
||||
"importDirectlyFromBrowser": {
|
||||
"message": "Import directly from browser"
|
||||
},
|
||||
"browserProfile": {
|
||||
"message": "Browser Profile"
|
||||
},
|
||||
"seeDetailedInstructions": {
|
||||
"message": "See detailed instructions on our help site at",
|
||||
"description": "This is followed a by a hyperlink to the help website."
|
||||
@@ -4080,5 +4086,23 @@
|
||||
"moreBreadcrumbs": {
|
||||
"message": "More breadcrumbs",
|
||||
"description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed."
|
||||
},
|
||||
"next": {
|
||||
"message": "Next"
|
||||
},
|
||||
"confirmKeyConnectorDomain": {
|
||||
"message": "Confirm Key Connector domain"
|
||||
},
|
||||
"confirm": {
|
||||
"message": "Confirm"
|
||||
},
|
||||
"enableAutotypeTransitionKey": {
|
||||
"message": "Enable autotype shortcut"
|
||||
},
|
||||
"enableAutotypeDescriptionTransitionKey": {
|
||||
"message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place."
|
||||
},
|
||||
"editShortcut": {
|
||||
"message": "Edit shortcut"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from "@bitwarden/state-internal";
|
||||
import { SerializedMemoryStorageService, StorageServiceProvider } from "@bitwarden/storage-core";
|
||||
|
||||
import { ChromiumImporterService } from "./app/tools/import/chromium-importer.service";
|
||||
import { MainDesktopAutotypeService } from "./autofill/main/main-desktop-autotype.service";
|
||||
import { MainSshAgentService } from "./autofill/main/main-ssh-agent.service";
|
||||
import { DesktopAutofillSettingsService } from "./autofill/services/desktop-autofill-settings.service";
|
||||
@@ -300,6 +301,8 @@ export class Main {
|
||||
this.ssoUrlService,
|
||||
);
|
||||
|
||||
new ChromiumImporterService();
|
||||
|
||||
this.nativeAutofillMain = new NativeAutofillMain(this.logService, this.windowMain);
|
||||
void this.nativeAutofillMain.init();
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export class WindowMain {
|
||||
private windowStateChangeTimer: NodeJS.Timeout;
|
||||
private windowStates: { [key: string]: WindowState } = {};
|
||||
private enableAlwaysOnTop = false;
|
||||
private enableRendererProcessForceCrashReload = false;
|
||||
private enableRendererProcessForceCrashReload = true;
|
||||
session: Electron.Session;
|
||||
|
||||
readonly defaultWidth = 950;
|
||||
@@ -149,28 +149,31 @@ export class WindowMain {
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on("ready", async () => {
|
||||
if (isMac() || isWindows()) {
|
||||
this.enableRendererProcessForceCrashReload = true;
|
||||
} else if (isLinux() && !isDev()) {
|
||||
if (await processisolations.isCoreDumpingDisabled()) {
|
||||
this.logService.info("Coredumps are disabled in renderer process");
|
||||
this.enableRendererProcessForceCrashReload = true;
|
||||
} else {
|
||||
this.logService.info("Disabling coredumps in main process");
|
||||
if (!isDev()) {
|
||||
// This currently breaks the file portal for snap https://github.com/flatpak/xdg-desktop-portal/issues/785
|
||||
if (!isSnapStore()) {
|
||||
this.logService.info(
|
||||
"[Process Isolation] Isolating process from debuggers and memory dumps",
|
||||
);
|
||||
try {
|
||||
await processisolations.disableCoredumps();
|
||||
await processisolations.isolateProcess();
|
||||
} catch (e) {
|
||||
this.logService.error("Failed to disable coredumps", e);
|
||||
this.logService.error("[Process Isolation] Failed to isolate main process", e);
|
||||
}
|
||||
}
|
||||
|
||||
// this currently breaks the file portal for snap https://github.com/flatpak/xdg-desktop-portal/issues/785
|
||||
if (!isSnapStore()) {
|
||||
this.logService.info("Disabling memory dumps in main process");
|
||||
try {
|
||||
await processisolations.disableMemoryAccess();
|
||||
} catch (e) {
|
||||
this.logService.error("Failed to disable memory dumps", e);
|
||||
if (isLinux()) {
|
||||
if (await processisolations.isCoreDumpingDisabled()) {
|
||||
this.logService.info("Coredumps are disabled in renderer process");
|
||||
} else {
|
||||
this.enableRendererProcessForceCrashReload = false;
|
||||
this.logService.info("Disabling coredumps in main process");
|
||||
try {
|
||||
await processisolations.disableCoredumps();
|
||||
this.enableRendererProcessForceCrashReload = true;
|
||||
} catch (e) {
|
||||
this.logService.error("Failed to disable coredumps", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { contextBridge } from "electron";
|
||||
|
||||
import tools from "./app/tools/preload";
|
||||
import auth from "./auth/preload";
|
||||
import autofill from "./autofill/preload";
|
||||
import keyManagement from "./key-management/preload";
|
||||
@@ -21,6 +22,7 @@ export const ipc = {
|
||||
autofill,
|
||||
platform,
|
||||
keyManagement,
|
||||
tools,
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld("ipc", ipc);
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<app-organization-free-trial-warning
|
||||
*ngIf="useOrganizationWarningsService$ | async"
|
||||
[organization]="organization"
|
||||
(clicked)="navigateToPaymentMethod()"
|
||||
>
|
||||
</app-organization-free-trial-warning>
|
||||
<app-organization-reseller-renewal-warning
|
||||
*ngIf="useOrganizationWarningsService$ | async"
|
||||
[organization]="organization"
|
||||
>
|
||||
</app-organization-reseller-renewal-warning>
|
||||
<ng-container *ngIf="freeTrialWhenWarningsServiceDisabled$ | async as freeTrial">
|
||||
<bit-banner
|
||||
id="free-trial-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
icon="bwi-billing"
|
||||
bannerType="premium"
|
||||
[showClose]="false"
|
||||
*ngIf="!refreshing && freeTrial.shownBanner"
|
||||
>
|
||||
{{ freeTrial.message }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="secondary"
|
||||
(click)="navigateToPaymentMethod()"
|
||||
class="tw-cursor-pointer"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ "clickHereToAddPaymentMethod" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="resellerWarningWhenWarningsServiceDisabled$ | async as resellerWarning">
|
||||
<bit-banner
|
||||
id="reseller-warning-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
icon="bwi-billing"
|
||||
bannerType="info"
|
||||
[showClose]="false"
|
||||
*ngIf="!refreshing"
|
||||
>
|
||||
{{ resellerWarning?.message }}
|
||||
</bit-banner>
|
||||
</ng-container>
|
||||
|
||||
<app-org-vault-header
|
||||
[filter]="filter"
|
||||
[loading]="refreshing"
|
||||
[organization]="organization"
|
||||
[collection]="selectedCollection"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(onAddCipher)="addCipher($event)"
|
||||
(onAddCollection)="addCollection()"
|
||||
(onEditCollection)="editCollection(selectedCollection.node, $event.tab, $event.readonly)"
|
||||
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-org-vault-header>
|
||||
|
||||
<div class="tw-flex tw-flex-row">
|
||||
<div class="tw-w-1/4 tw-mr-5" *ngIf="!hideVaultFilters">
|
||||
<app-organization-vault-filter
|
||||
[organization]="organization"
|
||||
[activeFilter]="activeFilter"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
<div [class]="hideVaultFilters ? 'tw-w-full' : 'tw-w-3/4'">
|
||||
<bit-toggle-group
|
||||
*ngIf="showAddAccessToggle && activeFilter.selectedCollectionNode"
|
||||
[selected]="addAccessStatus$ | async"
|
||||
(selectedChange)="addAccessToggle($event)"
|
||||
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||
>
|
||||
<bit-toggle [value]="0">
|
||||
{{ "all" | i18n }}
|
||||
</bit-toggle>
|
||||
|
||||
<bit-toggle [value]="1">
|
||||
{{ "addAccess" | i18n }}
|
||||
</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
<bit-callout type="warning" *ngIf="activeFilter.isDeleted">
|
||||
{{ trashCleanupWarning }}
|
||||
</bit-callout>
|
||||
<app-vault-items
|
||||
#vaultItems
|
||||
[ciphers]="ciphers"
|
||||
[collections]="collections"
|
||||
[allCollections]="allCollections"
|
||||
[allOrganizations]="organization ? [organization] : []"
|
||||
[allGroups]="allGroups"
|
||||
[disabled]="loading"
|
||||
[showOwner]="false"
|
||||
[showPermissionsColumn]="true"
|
||||
[showCollections]="filter.type !== undefined"
|
||||
[showGroups]="
|
||||
organization?.useGroups &&
|
||||
((filter.type === undefined && filter.collectionId === undefined) ||
|
||||
filter.collectionId !== undefined)
|
||||
"
|
||||
[showPremiumFeatures]="organization?.useTotp"
|
||||
[showBulkMove]="false"
|
||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||
[useEvents]="organization?.canAccessEventLogs"
|
||||
[showAdminActions]="true"
|
||||
(onEvent)="onVaultItemsEvent($event)"
|
||||
[showBulkEditCollectionAccess]="true"
|
||||
[showBulkAddToCollections]="true"
|
||||
[viewingOrgVault]="true"
|
||||
[addAccessStatus]="addAccessStatus$ | async"
|
||||
[addAccessToggle]="showAddAccessToggle"
|
||||
[activeCollection]="selectedCollection?.node"
|
||||
>
|
||||
</app-vault-items>
|
||||
<ng-container *ngIf="!performingInitialLoad && isEmpty">
|
||||
<bit-no-items *ngIf="!showCollectionAccessRestricted">
|
||||
<span slot="title" class="tw-mt-4 tw-block">{{ "noItemsInList" | i18n }}</span>
|
||||
<button
|
||||
slot="button"
|
||||
bitButton
|
||||
(click)="addCipher()"
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
*ngIf="
|
||||
filter.type !== 'trash' &&
|
||||
filter.collectionId !== Unassigned &&
|
||||
selectedCollection?.node?.canEditItems(organization)
|
||||
"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-plus"></i> {{ "newItem" | i18n }}
|
||||
</button>
|
||||
</bit-no-items>
|
||||
<collection-access-restricted
|
||||
*ngIf="showCollectionAccessRestricted"
|
||||
[canEditCollection]="selectedCollection?.node?.canEdit(organization)"
|
||||
[canViewCollectionInfo]="selectedCollection?.node?.canViewCollectionInfo(organization)"
|
||||
(viewCollectionClicked)="
|
||||
editCollection(selectedCollection.node, $event.tab, $event.readonly)
|
||||
"
|
||||
>
|
||||
</collection-access-restricted>
|
||||
</ng-container>
|
||||
<div
|
||||
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
|
||||
*ngIf="performingInitialLoad"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,28 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
||||
import { canAccessVaultTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
import { organizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
|
||||
import { VaultComponent } from "./vault.component";
|
||||
import { VaultComponent } from "./deprecated_vault.component";
|
||||
import { vNextVaultComponent } from "./vault.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
component: VaultComponent,
|
||||
canActivate: [organizationPermissionsGuard(canAccessVaultTab)],
|
||||
data: { titleId: "vaults" },
|
||||
},
|
||||
...featureFlaggedRoute({
|
||||
defaultComponent: VaultComponent,
|
||||
flaggedComponent: vNextVaultComponent,
|
||||
featureFlag: FeatureFlag.CollectionVaultRefactor,
|
||||
routeOptions: {
|
||||
data: { titleId: "vaults" },
|
||||
path: "",
|
||||
canActivate: [organizationPermissionsGuard(canAccessVaultTab)],
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
|
||||
@@ -1,156 +1,186 @@
|
||||
<app-organization-free-trial-warning
|
||||
*ngIf="useOrganizationWarningsService$ | async"
|
||||
[organization]="organization"
|
||||
(clicked)="navigateToPaymentMethod()"
|
||||
>
|
||||
</app-organization-free-trial-warning>
|
||||
<app-organization-reseller-renewal-warning
|
||||
*ngIf="useOrganizationWarningsService$ | async"
|
||||
[organization]="organization"
|
||||
>
|
||||
</app-organization-reseller-renewal-warning>
|
||||
<ng-container *ngIf="freeTrialWhenWarningsServiceDisabled$ | async as freeTrial">
|
||||
<bit-banner
|
||||
id="free-trial-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
icon="bwi-billing"
|
||||
bannerType="premium"
|
||||
[showClose]="false"
|
||||
*ngIf="!refreshing && freeTrial.shownBanner"
|
||||
>
|
||||
{{ freeTrial.message }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="secondary"
|
||||
(click)="navigateToPaymentMethod()"
|
||||
class="tw-cursor-pointer"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ "clickHereToAddPaymentMethod" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="resellerWarningWhenWarningsServiceDisabled$ | async as resellerWarning">
|
||||
<bit-banner
|
||||
id="reseller-warning-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
icon="bwi-billing"
|
||||
bannerType="info"
|
||||
[showClose]="false"
|
||||
*ngIf="!refreshing"
|
||||
>
|
||||
{{ resellerWarning?.message }}
|
||||
</bit-banner>
|
||||
</ng-container>
|
||||
@let organization = organization$ | async;
|
||||
@let selectedCollection = selectedCollection$ | async;
|
||||
@let filter = filter$ | async;
|
||||
@let refreshing = refreshingSubject$ | async;
|
||||
@let loading = loading$ | async;
|
||||
|
||||
<app-org-vault-header
|
||||
[filter]="filter"
|
||||
[loading]="refreshing"
|
||||
[organization]="organization"
|
||||
[collection]="selectedCollection"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(onAddCipher)="addCipher($event)"
|
||||
(onAddCollection)="addCollection()"
|
||||
(onEditCollection)="editCollection(selectedCollection.node, $event.tab, $event.readonly)"
|
||||
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-org-vault-header>
|
||||
|
||||
<div class="tw-flex tw-flex-row">
|
||||
<div class="tw-w-1/4 tw-mr-5" *ngIf="!hideVaultFilters">
|
||||
<app-organization-vault-filter
|
||||
@if (organization) {
|
||||
@if (useOrganizationWarningsService$ | async) {
|
||||
<app-organization-free-trial-warning
|
||||
[organization]="organization"
|
||||
[activeFilter]="activeFilter"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
<div [class]="hideVaultFilters ? 'tw-w-full' : 'tw-w-3/4'">
|
||||
<bit-toggle-group
|
||||
*ngIf="showAddAccessToggle && activeFilter.selectedCollectionNode"
|
||||
[selected]="addAccessStatus$ | async"
|
||||
(selectedChange)="addAccessToggle($event)"
|
||||
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||
(clicked)="navigateToPaymentMethod()"
|
||||
>
|
||||
<bit-toggle [value]="0">
|
||||
{{ "all" | i18n }}
|
||||
</bit-toggle>
|
||||
</app-organization-free-trial-warning>
|
||||
}
|
||||
|
||||
<bit-toggle [value]="1">
|
||||
{{ "addAccess" | i18n }}
|
||||
</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
<bit-callout type="warning" *ngIf="activeFilter.isDeleted">
|
||||
{{ trashCleanupWarning }}
|
||||
</bit-callout>
|
||||
<app-vault-items
|
||||
#vaultItems
|
||||
[ciphers]="ciphers"
|
||||
[collections]="collections"
|
||||
[allCollections]="allCollections"
|
||||
[allOrganizations]="organization ? [organization] : []"
|
||||
[allGroups]="allGroups"
|
||||
[disabled]="loading"
|
||||
[showOwner]="false"
|
||||
[showPermissionsColumn]="true"
|
||||
[showCollections]="filter.type !== undefined"
|
||||
[showGroups]="
|
||||
organization?.useGroups &&
|
||||
((filter.type === undefined && filter.collectionId === undefined) ||
|
||||
filter.collectionId !== undefined)
|
||||
"
|
||||
[showPremiumFeatures]="organization?.useTotp"
|
||||
[showBulkMove]="false"
|
||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||
[useEvents]="organization?.canAccessEventLogs"
|
||||
[showAdminActions]="true"
|
||||
(onEvent)="onVaultItemsEvent($event)"
|
||||
[showBulkEditCollectionAccess]="true"
|
||||
[showBulkAddToCollections]="true"
|
||||
[viewingOrgVault]="true"
|
||||
[addAccessStatus]="addAccessStatus$ | async"
|
||||
[addAccessToggle]="showAddAccessToggle"
|
||||
[activeCollection]="selectedCollection?.node"
|
||||
@if (useOrganizationWarningsService$ | async) {
|
||||
<app-organization-reseller-renewal-warning [organization]="organization">
|
||||
</app-organization-reseller-renewal-warning>
|
||||
}
|
||||
|
||||
@let freeTrial = freeTrialWhenWarningsServiceDisabled$ | async;
|
||||
@if (!refreshing && freeTrial?.shownBanner) {
|
||||
<bit-banner
|
||||
id="free-trial-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
icon="bwi-billing"
|
||||
bannerType="premium"
|
||||
[showClose]="false"
|
||||
>
|
||||
</app-vault-items>
|
||||
<ng-container *ngIf="!performingInitialLoad && isEmpty">
|
||||
<bit-no-items *ngIf="!showCollectionAccessRestricted">
|
||||
<span slot="title" class="tw-mt-4 tw-block">{{ "noItemsInList" | i18n }}</span>
|
||||
<button
|
||||
slot="button"
|
||||
bitButton
|
||||
(click)="addCipher()"
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
*ngIf="
|
||||
filter.type !== 'trash' &&
|
||||
filter.collectionId !== Unassigned &&
|
||||
selectedCollection?.node?.canEditItems(organization)
|
||||
"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-plus"></i> {{ "newItem" | i18n }}
|
||||
</button>
|
||||
</bit-no-items>
|
||||
<collection-access-restricted
|
||||
*ngIf="showCollectionAccessRestricted"
|
||||
[canEditCollection]="selectedCollection?.node?.canEdit(organization)"
|
||||
[canViewCollectionInfo]="selectedCollection?.node?.canViewCollectionInfo(organization)"
|
||||
(viewCollectionClicked)="
|
||||
editCollection(selectedCollection.node, $event.tab, $event.readonly)
|
||||
"
|
||||
{{ freeTrial.message }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="secondary"
|
||||
(click)="navigateToPaymentMethod()"
|
||||
class="tw-cursor-pointer"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
</collection-access-restricted>
|
||||
</ng-container>
|
||||
<div
|
||||
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
|
||||
*ngIf="performingInitialLoad"
|
||||
{{ "clickHereToAddPaymentMethod" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
}
|
||||
|
||||
@let resellerWarning = resellerWarningWhenWarningsServiceDisabled$ | async;
|
||||
@if (!refreshing && resellerWarning) {
|
||||
<bit-banner
|
||||
id="reseller-warning-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
icon="bwi-billing"
|
||||
bannerType="info"
|
||||
[showClose]="false"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
{{ resellerWarning?.message }}
|
||||
</bit-banner>
|
||||
}
|
||||
|
||||
@if (filter) {
|
||||
<app-org-vault-header
|
||||
[filter]="filter"
|
||||
[loading]="refreshing"
|
||||
[organization]="organization"
|
||||
[collection]="selectedCollection"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(onAddCipher)="addCipher($event)"
|
||||
(onAddCollection)="addCollection()"
|
||||
(onEditCollection)="editCollection(selectedCollection?.node, $event.tab, $event.readonly)"
|
||||
(onDeleteCollection)="deleteCollection(selectedCollection?.node)"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-org-vault-header>
|
||||
}
|
||||
|
||||
<div class="tw-flex tw-flex-row">
|
||||
@let hideVaultFilters = hideVaultFilter$ | async;
|
||||
@if (!hideVaultFilters) {
|
||||
<div class="tw-w-1/4 tw-mr-5">
|
||||
<app-organization-vault-filter
|
||||
[organization]="organization"
|
||||
[activeFilter]="activeFilter"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div [class]="hideVaultFilters ? 'tw-w-full' : 'tw-w-3/4'">
|
||||
@if (showAddAccessToggle && activeFilter.selectedCollectionNode) {
|
||||
<bit-toggle-group
|
||||
[selected]="addAccessStatus$ | async"
|
||||
(selectedChange)="addAccessToggle($event)"
|
||||
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||
>
|
||||
<bit-toggle [value]="0">
|
||||
{{ "all" | i18n }}
|
||||
</bit-toggle>
|
||||
|
||||
<bit-toggle [value]="1">
|
||||
{{ "addAccess" | i18n }}
|
||||
</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
}
|
||||
|
||||
@if (activeFilter.isDeleted) {
|
||||
<bit-callout type="warning">
|
||||
{{ trashCleanupWarning }}
|
||||
</bit-callout>
|
||||
}
|
||||
|
||||
@if (filter) {
|
||||
<app-vault-items
|
||||
#vaultItems
|
||||
[ciphers]="ciphers$ | async"
|
||||
[collections]="collections$ | async"
|
||||
[allCollections]="allCollections$ | async"
|
||||
[allOrganizations]="organization ? [organization] : []"
|
||||
[allGroups]="allGroups$ | async"
|
||||
[disabled]="loading"
|
||||
[showOwner]="false"
|
||||
[showPermissionsColumn]="true"
|
||||
[showCollections]="filter.type !== undefined"
|
||||
[showGroups]="
|
||||
organization?.useGroups &&
|
||||
((filter.type === undefined && filter.collectionId === undefined) ||
|
||||
filter.collectionId !== undefined)
|
||||
"
|
||||
[showPremiumFeatures]="organization?.useTotp"
|
||||
[showBulkMove]="false"
|
||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||
[useEvents]="organization?.canAccessEventLogs"
|
||||
[showAdminActions]="true"
|
||||
(onEvent)="onVaultItemsEvent($event)"
|
||||
[showBulkEditCollectionAccess]="true"
|
||||
[showBulkAddToCollections]="true"
|
||||
[viewingOrgVault]="true"
|
||||
[addAccessStatus]="addAccessStatus$ | async"
|
||||
[addAccessToggle]="showAddAccessToggle"
|
||||
[activeCollection]="selectedCollection?.node"
|
||||
>
|
||||
</app-vault-items>
|
||||
}
|
||||
|
||||
@let showCollectionAccessRestricted = showCollectionAccessRestricted$ | async;
|
||||
@if (!refreshing && (isEmpty$ | async)) {
|
||||
@if (!showCollectionAccessRestricted) {
|
||||
<bit-no-items>
|
||||
<span slot="title" class="tw-mt-4 tw-block">{{ "noItemsInList" | i18n }}</span>
|
||||
|
||||
@if (
|
||||
filter &&
|
||||
filter.type !== "trash" &&
|
||||
filter.collectionId !== Unassigned &&
|
||||
selectedCollection?.node?.canEditItems(organization)
|
||||
) {
|
||||
<button
|
||||
slot="button"
|
||||
bitButton
|
||||
(click)="addCipher()"
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-plus"></i> {{ "newItem" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</bit-no-items>
|
||||
} @else {
|
||||
<collection-access-restricted
|
||||
[canEditCollection]="selectedCollection?.node?.canEdit(organization)"
|
||||
[canViewCollectionInfo]="selectedCollection?.node?.canViewCollectionInfo(organization)"
|
||||
(viewCollectionClicked)="
|
||||
editCollection(selectedCollection?.node, $event.tab, $event.readonly)
|
||||
"
|
||||
>
|
||||
</collection-access-restricted>
|
||||
}
|
||||
}
|
||||
@if (refreshing) {
|
||||
<div class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,10 @@ import { ViewComponent } from "../../../vault/individual-vault/view.component";
|
||||
import { CollectionDialogComponent } from "../shared/components/collection-dialog";
|
||||
|
||||
import { CollectionNameBadgeComponent } from "./collection-badge";
|
||||
import { VaultComponent } from "./deprecated_vault.component";
|
||||
import { GroupBadgeModule } from "./group-badge/group-badge.module";
|
||||
import { VaultRoutingModule } from "./vault-routing.module";
|
||||
import { VaultComponent } from "./vault.component";
|
||||
import { vNextVaultComponent } from "./vault.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -19,6 +20,7 @@ import { VaultComponent } from "./vault.component";
|
||||
OrganizationBadgeModule,
|
||||
CollectionDialogComponent,
|
||||
VaultComponent,
|
||||
vNextVaultComponent,
|
||||
ViewComponent,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -55,10 +55,7 @@
|
||||
<bit-nav-item
|
||||
[text]="'eventLogs' | i18n"
|
||||
route="reporting/events"
|
||||
*ngIf="
|
||||
(organization.canAccessEventLogs && organization.useEvents) ||
|
||||
(organization.isOwner && (isBreadcrumbEventLogsEnabled$ | async))
|
||||
"
|
||||
*ngIf="organization.canAccessEventLogs || organization.isOwner"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
[text]="'reports' | i18n"
|
||||
@@ -102,7 +99,7 @@
|
||||
<bit-nav-item
|
||||
[text]="'policies' | i18n"
|
||||
route="settings/policies"
|
||||
*ngIf="canShowPoliciesTab$ | async"
|
||||
*ngIf="organization.canManagePolicies"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
[text]="'twoStepLogin' | i18n"
|
||||
|
||||
@@ -68,9 +68,7 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
hideNewOrgButton$: Observable<boolean>;
|
||||
organizationIsUnmanaged$: Observable<boolean>;
|
||||
|
||||
protected isBreadcrumbEventLogsEnabled$: Observable<boolean>;
|
||||
protected showSponsoredFamiliesDropdown$: Observable<boolean>;
|
||||
protected canShowPoliciesTab$: Observable<boolean>;
|
||||
|
||||
protected paymentDetailsPageData$: Observable<{
|
||||
route: string;
|
||||
@@ -94,9 +92,6 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isBreadcrumbEventLogsEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM12276_BreadcrumbEventLogs,
|
||||
);
|
||||
document.body.classList.remove("layout_frontend");
|
||||
|
||||
this.organization$ = this.route.params.pipe(
|
||||
@@ -141,18 +136,6 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
|
||||
this.integrationPageEnabled$ = this.organization$.pipe(map((org) => org.canAccessIntegrations));
|
||||
|
||||
this.canShowPoliciesTab$ = this.organization$.pipe(
|
||||
switchMap((organization) =>
|
||||
this.organizationBillingService
|
||||
.isBreadcrumbingPoliciesEnabled$(organization)
|
||||
.pipe(
|
||||
map(
|
||||
(isBreadcrumbingEnabled) => isBreadcrumbingEnabled || organization.canManagePolicies,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
this.paymentDetailsPageData$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout)
|
||||
.pipe(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@let usePlaceHolderEvents = !organization?.useEvents && (isBreadcrumbEventLogsEnabled$ | async);
|
||||
@let usePlaceHolderEvents = !organization?.useEvents;
|
||||
<app-header>
|
||||
<span
|
||||
bitBadge
|
||||
|
||||
@@ -19,7 +19,6 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||
import { EventSystemUser } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EventResponse } from "@bitwarden/common/models/response/event.response";
|
||||
import { EventView } from "@bitwarden/common/models/view/event.view";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -62,10 +61,6 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
|
||||
private orgUsersUserIdMap = new Map<string, any>();
|
||||
readonly ProductTierType = ProductTierType;
|
||||
|
||||
protected isBreadcrumbEventLogsEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM12276_BreadcrumbEventLogs,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private route: ActivatedRoute,
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
<app-header>
|
||||
@let organization = organization$ | async;
|
||||
@if (isBreadcrumbingEnabled$ | async) {
|
||||
<button
|
||||
bitBadge
|
||||
class="!tw-align-middle"
|
||||
(click)="changePlan(organization)"
|
||||
slot="title-suffix"
|
||||
type="button"
|
||||
variant="primary"
|
||||
>
|
||||
{{ "upgrade" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</app-header>
|
||||
<app-header></app-header>
|
||||
|
||||
<bit-container>
|
||||
@let organization = organization$ | async;
|
||||
@if (loading) {
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom, lastValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
import { firstValueFrom, lastValueFrom, Observable } from "rxjs";
|
||||
import { first, map } from "rxjs/operators";
|
||||
|
||||
import {
|
||||
getOrganizationById,
|
||||
@@ -14,18 +14,11 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
ChangePlanDialogResultType,
|
||||
openChangePlanDialog,
|
||||
} from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component";
|
||||
import { All } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
|
||||
import { PolicyListService } from "../../core/policy-list.service";
|
||||
import { BasePolicy } from "../policies";
|
||||
import { CollectionDialogTabType } from "../shared/components/collection-dialog";
|
||||
|
||||
import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component";
|
||||
|
||||
@@ -38,19 +31,17 @@ export class PoliciesComponent implements OnInit {
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
policies: BasePolicy[];
|
||||
protected organization$: Observable<Organization>;
|
||||
organization$: Observable<Organization>;
|
||||
|
||||
private orgPolicies: PolicyResponse[];
|
||||
protected policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
|
||||
protected isBreadcrumbingEnabled$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private accountService: AccountService,
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyListService: PolicyListService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private dialogService: DialogService,
|
||||
protected configService: ConfigService,
|
||||
) {}
|
||||
@@ -62,9 +53,11 @@ export class PoliciesComponent implements OnInit {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
this.organization$ = this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId));
|
||||
|
||||
this.policies = this.policyListService.getPolicies();
|
||||
|
||||
await this.load();
|
||||
@@ -100,11 +93,7 @@ export class PoliciesComponent implements OnInit {
|
||||
this.orgPolicies.forEach((op) => {
|
||||
this.policiesEnabledMap.set(op.type, op.enabled);
|
||||
});
|
||||
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
|
||||
switchMap((organization) =>
|
||||
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
|
||||
),
|
||||
);
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
@@ -117,34 +106,8 @@ export class PoliciesComponent implements OnInit {
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
switch (result) {
|
||||
case PolicyEditDialogResult.Saved:
|
||||
await this.load();
|
||||
break;
|
||||
case PolicyEditDialogResult.UpgradePlan:
|
||||
await this.changePlan(await firstValueFrom(this.organization$));
|
||||
break;
|
||||
if (result === PolicyEditDialogResult.Saved) {
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly CollectionDialogTabType = CollectionDialogTabType;
|
||||
protected readonly All = All;
|
||||
|
||||
protected async changePlan(organization: Organization) {
|
||||
const reference = openChangePlanDialog(this.dialogService, {
|
||||
data: {
|
||||
organizationId: organization.id,
|
||||
subscription: null,
|
||||
productTierType: organization.productTierType,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(reference.closed);
|
||||
|
||||
if (result === ChangePlanDialogResultType.Closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [loading]="loading" [title]="'editPolicy' | i18n" [subtitle]="policy.name | i18n">
|
||||
<ng-container bitDialogTitle>
|
||||
<button
|
||||
bitBadge
|
||||
class="!tw-align-middle"
|
||||
(click)="upgradePlan()"
|
||||
*ngIf="isBreadcrumbingEnabled$ | async"
|
||||
type="button"
|
||||
variant="primary"
|
||||
>
|
||||
{{ "planNameEnterprise" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container bitDialogContent>
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
@@ -30,7 +18,6 @@
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
*ngIf="!(isBreadcrumbingEnabled$ | async); else breadcrumbing"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="saveDisabled$ | async"
|
||||
@@ -39,11 +26,6 @@
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<ng-template #breadcrumbing>
|
||||
<button bitButton buttonType="primary" bitFormButton type="button" (click)="upgradePlan()">
|
||||
{{ "upgrade" | i18n }}
|
||||
</button>
|
||||
</ng-template>
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -9,20 +9,12 @@ import {
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { map, Observable, switchMap } from "rxjs";
|
||||
import { Observable, map } from "rxjs";
|
||||
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
@@ -45,7 +37,6 @@ export type PolicyEditDialogData = {
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum PolicyEditDialogResult {
|
||||
Saved = "saved",
|
||||
UpgradePlan = "upgrade-plan",
|
||||
}
|
||||
@Component({
|
||||
selector: "app-policy-edit",
|
||||
@@ -66,22 +57,15 @@ export class PolicyEditComponent implements AfterViewInit {
|
||||
formGroup = this.formBuilder.group({
|
||||
enabled: [this.enabled],
|
||||
});
|
||||
protected organization$: Observable<Organization>;
|
||||
protected isBreadcrumbingEnabled$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: PolicyEditDialogData,
|
||||
private accountService: AccountService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private organizationService: OrganizationService,
|
||||
private i18nService: I18nService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef<PolicyEditDialogResult>,
|
||||
private toastService: ToastService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
) {}
|
||||
|
||||
get policy(): BasePolicy {
|
||||
return this.data.policy;
|
||||
}
|
||||
@@ -115,16 +99,6 @@ export class PolicyEditComponent implements AfterViewInit {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
this.organization$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.organizationService.organizations$(userId)),
|
||||
getOrganizationById(this.data.organizationId),
|
||||
);
|
||||
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
|
||||
switchMap((organization) =>
|
||||
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
@@ -154,8 +128,4 @@ export class PolicyEditComponent implements AfterViewInit {
|
||||
static open = (dialogService: DialogService, config: DialogConfig<PolicyEditDialogData>) => {
|
||||
return dialogService.open<PolicyEditDialogResult>(PolicyEditComponent, config);
|
||||
};
|
||||
|
||||
protected upgradePlan(): void {
|
||||
this.dialogRef.close(PolicyEditDialogResult.UpgradePlan);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { inject, NgModule } from "@angular/core";
|
||||
import { CanMatchFn, RouterModule, Routes } from "@angular/router";
|
||||
import { map } from "rxjs";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessReportingTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ExposedPasswordsReportComponent } from "../../../dirt/reports/pages/organizations/exposed-passwords-report.component";
|
||||
@@ -26,11 +23,6 @@ import { EventsComponent } from "../manage/events.component";
|
||||
|
||||
import { ReportsHomeComponent } from "./reports-home.component";
|
||||
|
||||
const breadcrumbEventLogsPermission$: CanMatchFn = () =>
|
||||
inject(ConfigService)
|
||||
.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs)
|
||||
.pipe(map((breadcrumbEventLogs) => breadcrumbEventLogs === true));
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
@@ -92,24 +84,10 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
// Event routing is temporarily duplicated
|
||||
{
|
||||
path: "events",
|
||||
component: EventsComponent,
|
||||
canMatch: [breadcrumbEventLogsPermission$], // if this matches, the flag is ON
|
||||
canActivate: [
|
||||
organizationPermissionsGuard(
|
||||
(org) => (org.canAccessEventLogs && org.useEvents) || org.isOwner,
|
||||
),
|
||||
],
|
||||
data: {
|
||||
titleId: "eventLogs",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "events",
|
||||
component: EventsComponent,
|
||||
canActivate: [organizationPermissionsGuard((org) => org.canAccessEventLogs)],
|
||||
canActivate: [organizationPermissionsGuard((org) => org.canAccessEventLogs || org.isOwner)],
|
||||
data: {
|
||||
titleId: "eventLogs",
|
||||
},
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { NgModule, inject } from "@angular/core";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
|
||||
import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard";
|
||||
import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard";
|
||||
@@ -43,14 +41,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: "policies",
|
||||
component: PoliciesComponent,
|
||||
canActivate: [
|
||||
organizationPermissionsGuard((o: Organization) => {
|
||||
const organizationBillingService = inject(OrganizationBillingServiceAbstraction);
|
||||
return organizationBillingService
|
||||
.isBreadcrumbingPoliciesEnabled$(o)
|
||||
.pipe(map((isBreadcrumbingEnabled) => o.canManagePolicies || isBreadcrumbingEnabled));
|
||||
}),
|
||||
],
|
||||
canActivate: [organizationPermissionsGuard((org) => org.canManagePolicies)],
|
||||
data: {
|
||||
titleId: "policies",
|
||||
},
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
@@ -188,22 +187,16 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
await this.loadOrg(this.params.organizationId);
|
||||
}
|
||||
|
||||
const isBreadcrumbEventLogsEnabled = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs),
|
||||
this.organizationSelected.setAsyncValidators(
|
||||
freeOrgCollectionLimitValidator(
|
||||
this.organizations$,
|
||||
this.collectionService
|
||||
.encryptedCollections$(userId)
|
||||
.pipe(map((collections) => collections ?? [])),
|
||||
this.i18nService,
|
||||
),
|
||||
);
|
||||
|
||||
if (isBreadcrumbEventLogsEnabled) {
|
||||
this.organizationSelected.setAsyncValidators(
|
||||
freeOrgCollectionLimitValidator(
|
||||
this.organizations$,
|
||||
this.collectionService
|
||||
.encryptedCollections$(userId)
|
||||
.pipe(map((collections) => collections ?? [])),
|
||||
this.i18nService,
|
||||
),
|
||||
);
|
||||
this.formGroup.updateValueAndValidity();
|
||||
}
|
||||
this.formGroup.updateValueAndValidity();
|
||||
|
||||
this.organizationSelected.valueChanges
|
||||
.pipe(
|
||||
|
||||
@@ -42,7 +42,7 @@ export type TrialOrganizationType = Exclude<ProductTierType, ProductTierType.Fre
|
||||
export interface OrganizationInfo {
|
||||
name: string;
|
||||
email: string;
|
||||
type: TrialOrganizationType;
|
||||
type: TrialOrganizationType | null;
|
||||
}
|
||||
|
||||
export interface OrganizationCreatedEvent {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[showClose]="false"
|
||||
*ngIf="freeTrialData?.shownBanner"
|
||||
>
|
||||
{{ freeTrialData.message }}
|
||||
{{ freeTrialData?.message }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="secondary"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Location } from "@angular/common";
|
||||
import { Component, OnDestroy } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
@@ -47,21 +45,21 @@ import { FreeTrial } from "../../types/free-trial";
|
||||
standalone: false,
|
||||
})
|
||||
export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
organizationId: string;
|
||||
organizationId!: string;
|
||||
isUnpaid = false;
|
||||
accountCredit: number;
|
||||
accountCredit?: number;
|
||||
paymentSource?: PaymentSourceResponse;
|
||||
subscriptionStatus?: string;
|
||||
protected freeTrialData: FreeTrial;
|
||||
organization: Organization;
|
||||
organizationSubscriptionResponse: OrganizationSubscriptionResponse;
|
||||
protected freeTrialData?: FreeTrial;
|
||||
organization?: Organization;
|
||||
organizationSubscriptionResponse?: OrganizationSubscriptionResponse;
|
||||
|
||||
loading = true;
|
||||
|
||||
protected readonly Math = Math;
|
||||
launchPaymentModalAutomatically = false;
|
||||
|
||||
protected taxInformation: TaxInformation;
|
||||
protected taxInformation?: TaxInformation;
|
||||
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
@@ -104,7 +102,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
.subscribe();
|
||||
|
||||
const state = this.router.getCurrentNavigation()?.extras?.state;
|
||||
// incase the above state is undefined or null we use redundantState
|
||||
// In case the above state is undefined or null, we use redundantState
|
||||
const redundantState: any = location.getState();
|
||||
const queryParam = this.activatedRoute.snapshot.queryParamMap.get(
|
||||
"launchPaymentModalAutomatically",
|
||||
@@ -116,10 +114,8 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
Object.prototype.hasOwnProperty.call(redundantState, "launchPaymentModalAutomatically")
|
||||
) {
|
||||
this.launchPaymentModalAutomatically = redundantState.launchPaymentModalAutomatically;
|
||||
} else if (queryParam === "true") {
|
||||
this.launchPaymentModalAutomatically = true;
|
||||
} else {
|
||||
this.launchPaymentModalAutomatically = false;
|
||||
this.launchPaymentModalAutomatically = queryParam === "true";
|
||||
}
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
@@ -155,14 +151,21 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
this.paymentSource = paymentSource;
|
||||
this.subscriptionStatus = subscriptionStatus;
|
||||
this.taxInformation = taxInformation;
|
||||
this.isUnpaid = this.subscriptionStatus === "unpaid";
|
||||
|
||||
if (this.organizationId) {
|
||||
const organizationSubscriptionPromise = this.organizationApiService.getSubscription(
|
||||
this.organizationId,
|
||||
);
|
||||
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("User ID is not found");
|
||||
}
|
||||
|
||||
const organizationPromise = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
@@ -173,15 +176,20 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
organizationSubscriptionPromise,
|
||||
organizationPromise,
|
||||
]);
|
||||
|
||||
if (!this.organization) {
|
||||
throw new Error("Organization is not found");
|
||||
}
|
||||
if (!this.paymentSource) {
|
||||
throw new Error("Payment source is not found");
|
||||
}
|
||||
|
||||
this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
||||
this.organization,
|
||||
this.organizationSubscriptionResponse,
|
||||
paymentSource,
|
||||
this.paymentSource,
|
||||
);
|
||||
}
|
||||
// TODO: Eslint upgrade. Please resolve this since the ?? does nothing
|
||||
// eslint-disable-next-line no-constant-binary-expression
|
||||
this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false;
|
||||
// If the flag `launchPaymentModalAutomatically` is set to true,
|
||||
// we schedule a timeout (delay of 800ms) to automatically launch the payment modal.
|
||||
// This delay ensures that any prior UI/rendering operations complete before triggering the modal.
|
||||
@@ -219,14 +227,14 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
const dialogRef = TrialPaymentDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organizationId,
|
||||
subscription: this.organizationSubscriptionResponse,
|
||||
productTierType: this.organization?.productTierType,
|
||||
subscription: this.organizationSubscriptionResponse!,
|
||||
productTierType: this.organization!.productTierType,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) {
|
||||
this.location.replaceState(this.location.path(), "", {});
|
||||
if (this.launchPaymentModalAutomatically && !this.organization.enabled) {
|
||||
if (this.launchPaymentModalAutomatically && !this.organization?.enabled) {
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
this.launchPaymentModalAutomatically = false;
|
||||
@@ -238,13 +246,14 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
await this.billingApiService.verifyOrganizationBankAccount(this.organizationId, request);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
title: "",
|
||||
message: this.i18nService.t("verifiedBankAccount"),
|
||||
});
|
||||
};
|
||||
|
||||
protected get accountCreditHeaderText(): string {
|
||||
const key = this.accountCredit <= 0 ? "accountBalance" : "accountCredit";
|
||||
const hasAccountCredit = this.accountCredit && this.accountCredit > 0;
|
||||
const key = hasAccountCredit ? "accountCredit" : "accountBalance";
|
||||
return this.i18nService.t(key);
|
||||
}
|
||||
|
||||
@@ -279,7 +288,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
if (!hasBillingAddress) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
title: "",
|
||||
message: this.i18nService.t("billingAddressRequiredToAddCredit"),
|
||||
});
|
||||
return false;
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import { PaymentComponent } from "../payment/payment.component";
|
||||
|
||||
export interface AdjustPaymentDialogParams {
|
||||
initialPaymentMethod?: PaymentMethodType;
|
||||
initialPaymentMethod?: PaymentMethodType | null;
|
||||
organizationId?: string;
|
||||
productTier?: ProductTierType;
|
||||
providerId?: string;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Location } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormControl, Validators } from "@angular/forms";
|
||||
@@ -42,21 +40,21 @@ import {
|
||||
export class PaymentMethodComponent implements OnInit, OnDestroy {
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
billing: BillingPaymentResponse;
|
||||
org: OrganizationSubscriptionResponse;
|
||||
sub: SubscriptionResponse;
|
||||
billing?: BillingPaymentResponse;
|
||||
org?: OrganizationSubscriptionResponse;
|
||||
sub?: SubscriptionResponse;
|
||||
paymentMethodType = PaymentMethodType;
|
||||
organizationId: string;
|
||||
organizationId?: string;
|
||||
isUnpaid = false;
|
||||
organization: Organization;
|
||||
organization?: Organization;
|
||||
|
||||
verifyBankForm = this.formBuilder.group({
|
||||
amount1: new FormControl<number>(null, [
|
||||
amount1: new FormControl<number>(0, [
|
||||
Validators.required,
|
||||
Validators.max(99),
|
||||
Validators.min(0),
|
||||
]),
|
||||
amount2: new FormControl<number>(null, [
|
||||
amount2: new FormControl<number>(0, [
|
||||
Validators.required,
|
||||
Validators.max(99),
|
||||
Validators.min(0),
|
||||
@@ -64,7 +62,7 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
launchPaymentModalAutomatically = false;
|
||||
protected freeTrialData: FreeTrial;
|
||||
protected freeTrialData?: FreeTrial;
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
@@ -84,7 +82,7 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
const state = this.router.getCurrentNavigation()?.extras?.state;
|
||||
// incase the above state is undefined or null we use redundantState
|
||||
// In case the above state is undefined or null, we use redundantState
|
||||
const redundantState: any = location.getState();
|
||||
if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) {
|
||||
this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically;
|
||||
@@ -129,17 +127,23 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.loading = true;
|
||||
if (this.forOrganization) {
|
||||
const billingPromise = this.organizationApiService.getBilling(this.organizationId);
|
||||
const billingPromise = this.organizationApiService.getBilling(this.organizationId!);
|
||||
const organizationSubscriptionPromise = this.organizationApiService.getSubscription(
|
||||
this.organizationId,
|
||||
this.organizationId!,
|
||||
);
|
||||
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("User ID is not found");
|
||||
}
|
||||
|
||||
const organizationPromise = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
.pipe(getOrganizationById(this.organizationId!)),
|
||||
);
|
||||
|
||||
[this.billing, this.org, this.organization] = await Promise.all([
|
||||
@@ -171,14 +175,16 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
addCredit = async () => {
|
||||
const dialogRef = openAddCreditDialog(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organizationId,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === AddCreditDialogResult.Added) {
|
||||
await this.load();
|
||||
if (this.forOrganization) {
|
||||
const dialogRef = openAddCreditDialog(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organizationId!,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === AddCreditDialogResult.Added) {
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -194,7 +200,7 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (result === AdjustPaymentDialogResultType.Submitted) {
|
||||
this.location.replaceState(this.location.path(), "", {});
|
||||
if (this.launchPaymentModalAutomatically && !this.organization.enabled) {
|
||||
if (this.launchPaymentModalAutomatically && !this.organization?.enabled) {
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
this.launchPaymentModalAutomatically = false;
|
||||
@@ -208,18 +214,22 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
const request = new VerifyBankRequest();
|
||||
request.amount1 = this.verifyBankForm.value.amount1;
|
||||
request.amount2 = this.verifyBankForm.value.amount2;
|
||||
await this.organizationApiService.verifyBank(this.organizationId, request);
|
||||
request.amount1 = this.verifyBankForm.value.amount1!;
|
||||
request.amount2 = this.verifyBankForm.value.amount2!;
|
||||
await this.organizationApiService.verifyBank(this.organizationId!, request);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
title: "",
|
||||
message: this.i18nService.t("verifiedBankAccount"),
|
||||
});
|
||||
await this.load();
|
||||
};
|
||||
|
||||
determineOrgsWithUpcomingPaymentIssues() {
|
||||
if (!this.organization || !this.org || !this.billing) {
|
||||
throw new Error("Organization, organization subscription, or billing is not defined");
|
||||
}
|
||||
|
||||
this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
||||
this.organization,
|
||||
this.org,
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="orgInfoFormGroup.controls.name.invalid"
|
||||
[loading]="loading && (trialPaymentOptional$ | async)"
|
||||
[loading]="loading && (trialPaymentOptional$ | async)!"
|
||||
(click)="orgNameEntrySubmit()"
|
||||
>
|
||||
{{
|
||||
@@ -55,8 +55,8 @@
|
||||
<app-trial-billing-step
|
||||
*ngIf="stepper.selectedIndex === 2"
|
||||
[organizationInfo]="{
|
||||
name: orgInfoFormGroup.value.name,
|
||||
email: orgInfoFormGroup.value.billingEmail,
|
||||
name: orgInfoFormGroup.value.name!,
|
||||
email: orgInfoFormGroup.value.billingEmail!,
|
||||
type: trialOrganizationType,
|
||||
}"
|
||||
[subscriptionProduct]="
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { StepperSelectionEvent } from "@angular/cdk/stepper";
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
@@ -50,15 +48,15 @@ export type InitiationPath =
|
||||
standalone: false,
|
||||
})
|
||||
export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||
@ViewChild("stepper", { static: false }) verticalStepper!: VerticalStepperComponent;
|
||||
|
||||
inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAccountRegistration;
|
||||
initializing = true;
|
||||
|
||||
/** Password Manager or Secrets Manager */
|
||||
product: ProductType;
|
||||
product?: ProductType;
|
||||
/** The tier of product being subscribed to */
|
||||
productTier: ProductTierType;
|
||||
productTier!: ProductTierType;
|
||||
/** Product types that display steppers for Password Manager */
|
||||
stepperProductTypes: ProductTierType[] = [
|
||||
ProductTierType.Teams,
|
||||
@@ -79,16 +77,16 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
orgId = "";
|
||||
orgLabel = "";
|
||||
billingSubLabel = "";
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
enforcedPolicyOptions?: MasterPasswordPolicyOptions;
|
||||
|
||||
/** User's email address associated with the trial */
|
||||
email = "";
|
||||
/** Token from the backend associated with the email verification */
|
||||
emailVerificationToken: string;
|
||||
emailVerificationToken?: string;
|
||||
loading = false;
|
||||
productTierValue: number;
|
||||
productTierValue?: ProductTierType;
|
||||
|
||||
trialLength: number;
|
||||
trialLength!: number;
|
||||
|
||||
orgInfoFormGroup = this.formBuilder.group({
|
||||
name: ["", { validators: [Validators.required, Validators.maxLength(50)], updateOn: "change" }],
|
||||
@@ -132,7 +130,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
// Show email validation toast when coming from email
|
||||
if (qParams.fromEmail && qParams.fromEmail === "true") {
|
||||
this.toastService.showToast({
|
||||
title: null,
|
||||
title: "",
|
||||
message: this.i18nService.t("emailVerifiedV2"),
|
||||
variant: "success",
|
||||
});
|
||||
@@ -172,9 +170,15 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
const invite = await this.organizationInviteService.getOrganizationInvite();
|
||||
let policies: Policy[] | null = null;
|
||||
let policies: Policy[] | undefined | null = null;
|
||||
|
||||
if (invite != null) {
|
||||
if (
|
||||
invite != null &&
|
||||
invite.organizationId &&
|
||||
invite.token &&
|
||||
invite.email &&
|
||||
invite.organizationUserId
|
||||
) {
|
||||
try {
|
||||
policies = await this.policyApiService.getPoliciesByToken(
|
||||
invite.organizationId,
|
||||
@@ -218,7 +222,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
if (event.selectedIndex === 1 && this.orgInfoFormGroup.controls.name.value === "") {
|
||||
this.orgInfoSubLabel = this.planInfoLabel;
|
||||
} else if (event.previouslySelectedIndex === 1) {
|
||||
this.orgInfoSubLabel = this.orgInfoFormGroup.controls.name.value;
|
||||
this.orgInfoSubLabel = this.orgInfoFormGroup.controls.name.value!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,8 +264,11 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
const organization: OrganizationInformation = {
|
||||
name: this.orgInfoFormGroup.value.name,
|
||||
billingEmail: this.orgInfoFormGroup.value.billingEmail,
|
||||
name: this.orgInfoFormGroup.value.name == null ? "" : this.orgInfoFormGroup.value.name,
|
||||
billingEmail:
|
||||
this.orgInfoFormGroup.value.billingEmail == null
|
||||
? ""
|
||||
: this.orgInfoFormGroup.value.billingEmail,
|
||||
initiationPath: trialInitiationPath,
|
||||
};
|
||||
|
||||
@@ -326,7 +333,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
get trialOrganizationType(): TrialOrganizationType {
|
||||
get trialOrganizationType(): TrialOrganizationType | null {
|
||||
if (this.productTier === ProductTierType.Free) {
|
||||
return null;
|
||||
}
|
||||
@@ -352,8 +359,12 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
|
||||
const response = await this.organizationBillingService.startFree({
|
||||
organization: {
|
||||
name: this.orgInfoFormGroup.value.name,
|
||||
billingEmail: this.orgInfoFormGroup.value.billingEmail,
|
||||
name: this.orgInfoFormGroup.value.name == null ? "" : this.orgInfoFormGroup.value.name,
|
||||
billingEmail:
|
||||
this.orgInfoFormGroup.value.billingEmail == null
|
||||
? ""
|
||||
: this.orgInfoFormGroup.value.billingEmail,
|
||||
initiationPath: "Password Manager trial from marketing website",
|
||||
},
|
||||
plan: {
|
||||
type: 0,
|
||||
@@ -405,11 +416,11 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
await this.loginStrategyService.logIn(credentials);
|
||||
}
|
||||
|
||||
finishRegistration(passwordInputResult: PasswordInputResult) {
|
||||
async finishRegistration(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
return this.registrationFinishService
|
||||
.finishRegistration(this.email, passwordInputResult, this.emailVerificationToken)
|
||||
.catch((e) => {
|
||||
.catch((e: unknown): null => {
|
||||
this.validationService.showError(e);
|
||||
this.submitting = false;
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ConfirmKeyConnectorDomainComponent as BaseConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui";
|
||||
import { RouterService } from "@bitwarden/web-vault/app/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-confirm-key-connector-domain",
|
||||
template: ` <confirm-key-connector-domain [onBeforeNavigation]="onBeforeNavigation" /> `,
|
||||
standalone: true,
|
||||
imports: [BaseConfirmKeyConnectorDomainComponent],
|
||||
})
|
||||
export class ConfirmKeyConnectorDomainComponent {
|
||||
constructor(private routerService: RouterService) {}
|
||||
|
||||
onBeforeNavigation = async () => {
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
};
|
||||
}
|
||||
@@ -67,6 +67,7 @@ import { freeTrialTextResolver } from "./billing/trial-initiation/complete-trial
|
||||
import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component";
|
||||
import { RouteDataProperties } from "./core";
|
||||
import { ReportsModule } from "./dirt/reports";
|
||||
import { ConfirmKeyConnectorDomainComponent } from "./key-management/key-connector/confirm-key-connector-domain.component";
|
||||
import { RemovePasswordComponent } from "./key-management/key-connector/remove-password.component";
|
||||
import { FrontendLayoutComponent } from "./layouts/frontend-layout.component";
|
||||
import { UserLayoutComponent } from "./layouts/user-layout.component";
|
||||
@@ -511,6 +512,17 @@ const routes: Routes = [
|
||||
titleId: "removeMasterPassword",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "confirm-key-connector-domain",
|
||||
component: ConfirmKeyConnectorDomainComponent,
|
||||
canActivate: [],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "confirmKeyConnectorDomain",
|
||||
},
|
||||
titleId: "confirmKeyConnectorDomain",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "trial-initiation",
|
||||
canActivate: [unauthGuardFn()],
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -218,28 +217,22 @@ export class VaultHeaderComponent {
|
||||
}
|
||||
|
||||
async addCollection(): Promise<void> {
|
||||
const isBreadcrumbEventLogsEnabled = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs),
|
||||
const organization = this.organizations?.find(
|
||||
(org) => org.productTierType === ProductTierType.Free,
|
||||
);
|
||||
|
||||
if (isBreadcrumbEventLogsEnabled) {
|
||||
const organization = this.organizations?.find(
|
||||
(org) => org.productTierType === ProductTierType.Free,
|
||||
);
|
||||
|
||||
if (this.organizations?.length == 1 && !!organization) {
|
||||
const collections = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.collectionAdminService.collectionAdminViews$(organization.id, userId),
|
||||
),
|
||||
if (this.organizations?.length == 1 && !!organization) {
|
||||
const collections = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.collectionAdminService.collectionAdminViews$(organization.id, userId),
|
||||
),
|
||||
);
|
||||
if (collections.length === organization.maxCollections) {
|
||||
await this.showFreeOrgUpgradeDialog(organization);
|
||||
return;
|
||||
}
|
||||
),
|
||||
);
|
||||
if (collections.length === organization.maxCollections) {
|
||||
await this.showFreeOrgUpgradeDialog(organization);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11166,6 +11166,9 @@
|
||||
"example": "92873837267"
|
||||
}
|
||||
}
|
||||
},
|
||||
"confirmKeyConnectorDomain": {
|
||||
"message": "Confirm Key Connector domain"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
bitwarden_license/bit-browser/jest.config.js
Normal file
22
bitwarden_license/bit-browser/jest.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
|
||||
const { compilerOptions } = require("../../tsconfig.base");
|
||||
|
||||
const sharedConfig = require("../../libs/shared/jest.config.angular");
|
||||
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
setupFilesAfterEnv: ["../../apps/browser/test.setup.ts"],
|
||||
moduleNameMapper: pathsToModuleNameMapper(
|
||||
{
|
||||
"@bitwarden/common/spec": ["libs/common/spec"],
|
||||
"@bitwarden/common": ["libs/common/src/*"],
|
||||
"@bitwarden/admin-console/common": ["libs/admin-console/src/common"],
|
||||
...(compilerOptions?.paths ?? {}),
|
||||
},
|
||||
{
|
||||
prefix: "<rootDir>/../../",
|
||||
},
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import OssMainBackground from "@bitwarden/browser/background/main.background";
|
||||
|
||||
export default class MainBackground {
|
||||
private ossMain = new OssMainBackground();
|
||||
|
||||
async bootstrap() {
|
||||
await this.ossMain.bootstrap();
|
||||
}
|
||||
}
|
||||
7
bitwarden_license/bit-browser/src/platform/background.ts
Normal file
7
bitwarden_license/bit-browser/src/platform/background.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
|
||||
import MainBackground from "../background/main.background";
|
||||
|
||||
const logService = new ConsoleLogService(false);
|
||||
const bitwardenMain = ((self as any).bitwardenMain = new MainBackground());
|
||||
bitwardenMain.bootstrap().catch((error) => logService.error(error));
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
const routes: Routes = [];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
14
bitwarden_license/bit-browser/src/popup/app.component.ts
Normal file
14
bitwarden_license/bit-browser/src/popup/app.component.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { AppComponent as BaseAppComponent } from "@bitwarden/browser/popup/app.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-root",
|
||||
templateUrl: "../../../../apps/browser/src/popup/app.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent extends BaseAppComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
return super.ngOnInit();
|
||||
}
|
||||
}
|
||||
39
bitwarden_license/bit-browser/src/popup/app.module.ts
Normal file
39
bitwarden_license/bit-browser/src/popup/app.module.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { OverlayModule } from "@angular/cdk/overlay";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
// import { AppRoutingAnimationsModule } from "@bitwarden/browser/popup/app-routing-animations";
|
||||
import { AppRoutingModule as OssRoutingModule } from "@bitwarden/browser/popup/app-routing.module";
|
||||
import { AppModule as OssModule } from "@bitwarden/browser/popup/app.module";
|
||||
// import { WildcardRoutingModule } from "@bitwarden/browser/popup/wildcard-routing.module";
|
||||
|
||||
import { AppRoutingModule } from "./app-routing.module";
|
||||
import { AppComponent } from "./app.component";
|
||||
/**
|
||||
* This is the AppModule for the commercial version of Bitwarden.
|
||||
* `apps/browser/app.module.ts` contains the OSS version.
|
||||
*
|
||||
* You probably do not want to modify this file. Consider editing `oss.module.ts` instead.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
OverlayModule,
|
||||
OssModule,
|
||||
JslibModule,
|
||||
// BrowserAnimationsModule,
|
||||
// FormsModule,
|
||||
// ReactiveFormsModule,
|
||||
// CoreModule,
|
||||
// DragDropModule,
|
||||
AppRoutingModule,
|
||||
OssRoutingModule,
|
||||
RouterModule,
|
||||
// WildcardRoutingModule, // Needs to be last to catch all non-existing routes
|
||||
],
|
||||
declarations: [AppComponent],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
26
bitwarden_license/bit-browser/src/popup/main.ts
Normal file
26
bitwarden_license/bit-browser/src/popup/main.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { enableProdMode } from "@angular/core";
|
||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
|
||||
import { PopupSizeService } from "@bitwarden/browser/platform/popup/layout/popup-size.service";
|
||||
import { BrowserPlatformUtilsService } from "@bitwarden/browser/platform/services/platform-utils/browser-platform-utils.service";
|
||||
|
||||
import { AppModule } from "./app.module";
|
||||
|
||||
import "@bitwarden/browser/popup/scss";
|
||||
|
||||
// We put these first to minimize the delay in window changing.
|
||||
PopupSizeService.initBodyWidthFromLocalStorage();
|
||||
// Should be removed once we deprecate support for Safari 16.0 and older. See Jira ticket [PM-1861]
|
||||
if (BrowserPlatformUtilsService.shouldApplySafariHeightFix(window)) {
|
||||
document.documentElement.classList.add("safari_height_fix");
|
||||
}
|
||||
|
||||
if (process.env.ENV === "production") {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
function init() {
|
||||
void platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
}
|
||||
|
||||
init();
|
||||
20
bitwarden_license/bit-browser/tsconfig.json
Normal file
20
bitwarden_license/bit-browser/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "../../apps/browser/tsconfig",
|
||||
"include": [
|
||||
"src",
|
||||
|
||||
"../../apps/browser/src/**/*.d.ts",
|
||||
|
||||
"../../apps/browser/src/autofill/content/*.ts",
|
||||
"../../apps/browser/src/autofill/fido2/content/*.ts",
|
||||
"../../apps/browser/src/autofill/notification/bar.ts",
|
||||
"../../apps/browser/src/autofill/overlay/inline-menu/**/*.ts",
|
||||
"../../apps/browser/src/platform/ipc/content/*.ts",
|
||||
"../../apps/browser/src/platform/offscreen-document/offscreen-document.ts",
|
||||
"../../apps/browser/src/popup/polyfills.ts",
|
||||
"../../apps/browser/src/vault/content/*.ts",
|
||||
|
||||
"../../libs/common/src/autofill/constants",
|
||||
"../../libs/common/custom-matchers.d.ts"
|
||||
]
|
||||
}
|
||||
8
bitwarden_license/bit-browser/tsconfig.spec.json
Normal file
8
bitwarden_license/bit-browser/tsconfig.spec.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": true,
|
||||
"emitDecoratorMetadata": false
|
||||
},
|
||||
"files": ["../../apps/browser/test.setup.ts"]
|
||||
}
|
||||
13
bitwarden_license/bit-browser/webpack.config.js
Normal file
13
bitwarden_license/bit-browser/webpack.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const { buildConfig } = require("../../apps/browser/webpack.base");
|
||||
|
||||
module.exports = buildConfig({
|
||||
configName: "Commercial",
|
||||
popup: {
|
||||
entry: "../../bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
entryModule: "../../bitwarden_license/bit-browser/src/popup/app.module#AppModule",
|
||||
},
|
||||
background: {
|
||||
entry: "../../bitwarden_license/bit-browser/src/platform/background.ts",
|
||||
},
|
||||
tsConfig: "../../bitwarden_license/bit-browser/tsconfig.json",
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user