merge main, fix conflicts
This commit is contained in:
commit
d74561d374
|
@ -173,63 +173,63 @@ jobs:
|
||||||
working-directory: browser-source/apps/browser
|
working-directory: browser-source/apps/browser
|
||||||
|
|
||||||
- name: Upload Opera artifact
|
- name: Upload Opera artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: dist-opera-${{ env._BUILD_NUMBER }}.zip
|
name: dist-opera-${{ env._BUILD_NUMBER }}.zip
|
||||||
path: browser-source/apps/browser/dist/dist-opera.zip
|
path: browser-source/apps/browser/dist/dist-opera.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Opera MV3 artifact (DO NOT USE FOR PROD)
|
- name: Upload Opera MV3 artifact (DO NOT USE FOR PROD)
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: DO-NOT-USE-FOR-PROD-dist-opera-MV3-${{ env._BUILD_NUMBER }}.zip
|
name: DO-NOT-USE-FOR-PROD-dist-opera-MV3-${{ env._BUILD_NUMBER }}.zip
|
||||||
path: browser-source/apps/browser/dist/dist-opera-mv3.zip
|
path: browser-source/apps/browser/dist/dist-opera-mv3.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Chrome MV3 artifact
|
- name: Upload Chrome MV3 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip
|
name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip
|
||||||
path: browser-source/apps/browser/dist/dist-chrome-mv3.zip
|
path: browser-source/apps/browser/dist/dist-chrome-mv3.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD)
|
- name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD)
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip
|
name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip
|
||||||
path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip
|
path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Firefox artifact
|
- name: Upload Firefox artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: dist-firefox-${{ env._BUILD_NUMBER }}.zip
|
name: dist-firefox-${{ env._BUILD_NUMBER }}.zip
|
||||||
path: browser-source/apps/browser/dist/dist-firefox.zip
|
path: browser-source/apps/browser/dist/dist-firefox.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Firefox MV3 artifact (DO NOT USE FOR PROD)
|
- name: Upload Firefox MV3 artifact (DO NOT USE FOR PROD)
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: DO-NOT-USE-FOR-PROD-dist-firefox-MV3-${{ env._BUILD_NUMBER }}.zip
|
name: DO-NOT-USE-FOR-PROD-dist-firefox-MV3-${{ env._BUILD_NUMBER }}.zip
|
||||||
path: browser-source/apps/browser/dist/dist-firefox-mv3.zip
|
path: browser-source/apps/browser/dist/dist-firefox-mv3.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Edge artifact
|
- name: Upload Edge artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: dist-edge-${{ env._BUILD_NUMBER }}.zip
|
name: dist-edge-${{ env._BUILD_NUMBER }}.zip
|
||||||
path: browser-source/apps/browser/dist/dist-edge.zip
|
path: browser-source/apps/browser/dist/dist-edge.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Edge MV3 artifact (DO NOT USE FOR PROD)
|
- name: Upload Edge MV3 artifact (DO NOT USE FOR PROD)
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: DO-NOT-USE-FOR-PROD-dist-edge-MV3-${{ env._BUILD_NUMBER }}.zip
|
name: DO-NOT-USE-FOR-PROD-dist-edge-MV3-${{ env._BUILD_NUMBER }}.zip
|
||||||
path: browser-source/apps/browser/dist/dist-edge-mv3.zip
|
path: browser-source/apps/browser/dist/dist-edge-mv3.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload browser source
|
- name: Upload browser source
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: browser-source-${{ env._BUILD_NUMBER }}.zip
|
name: browser-source-${{ env._BUILD_NUMBER }}.zip
|
||||||
path: browser-source.zip
|
path: browser-source.zip
|
||||||
|
@ -237,7 +237,7 @@ jobs:
|
||||||
|
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: false
|
if: false
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ env._BUILD_NUMBER }}.zip
|
name: coverage-${{ env._BUILD_NUMBER }}.zip
|
||||||
path: browser-source/apps/browser/coverage/coverage-${{ env._BUILD_NUMBER }}.zip
|
path: browser-source/apps/browser/coverage/coverage-${{ env._BUILD_NUMBER }}.zip
|
||||||
|
@ -352,7 +352,7 @@ jobs:
|
||||||
ls -la
|
ls -la
|
||||||
|
|
||||||
- name: Upload Safari artifact
|
- name: Upload Safari artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: dist-safari-${{ env._BUILD_NUMBER }}.zip
|
name: dist-safari-${{ env._BUILD_NUMBER }}.zip
|
||||||
path: apps/browser/dist/dist-safari.zip
|
path: apps/browser/dist/dist-safari.zip
|
||||||
|
@ -382,7 +382,7 @@ jobs:
|
||||||
secrets: "crowdin-api-token"
|
secrets: "crowdin-api-token"
|
||||||
|
|
||||||
- name: Upload Sources
|
- name: Upload Sources
|
||||||
uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0
|
uses: crowdin/github-action@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||||
|
|
|
@ -130,14 +130,14 @@ jobs:
|
||||||
matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
|
matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||||
|
|
||||||
- name: Upload unix zip asset
|
- name: Upload unix zip asset
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip
|
name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip
|
||||||
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip
|
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload unix checksum asset
|
- name: Upload unix checksum asset
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
|
name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||||
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
|
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||||
|
@ -269,14 +269,14 @@ jobs:
|
||||||
-t sha256 | Out-File -Encoding ASCII ./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${env:_PACKAGE_VERSION}.txt
|
-t sha256 | Out-File -Encoding ASCII ./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${env:_PACKAGE_VERSION}.txt
|
||||||
|
|
||||||
- name: Upload windows zip asset
|
- name: Upload windows zip asset
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip
|
name: bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||||
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip
|
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload windows checksum asset
|
- name: Upload windows checksum asset
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
name: bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||||
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||||
|
@ -284,7 +284,7 @@ jobs:
|
||||||
|
|
||||||
- name: Upload Chocolatey asset
|
- name: Upload Chocolatey asset
|
||||||
if: matrix.license_type.build_prefix == 'bit'
|
if: matrix.license_type.build_prefix == 'bit'
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
|
name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
|
||||||
path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
|
path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
|
||||||
|
@ -295,7 +295,7 @@ jobs:
|
||||||
|
|
||||||
- name: Upload NPM Build Directory asset
|
- name: Upload NPM Build Directory asset
|
||||||
if: matrix.license_type.build_prefix == 'bit'
|
if: matrix.license_type.build_prefix == 'bit'
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip
|
name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip
|
||||||
path: apps/cli/bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip
|
path: apps/cli/bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip
|
||||||
|
@ -364,14 +364,14 @@ jobs:
|
||||||
run: sudo snap remove bw
|
run: sudo snap remove bw
|
||||||
|
|
||||||
- name: Upload snap asset
|
- name: Upload snap asset
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bw_${{ env._PACKAGE_VERSION }}_amd64.snap
|
name: bw_${{ env._PACKAGE_VERSION }}_amd64.snap
|
||||||
path: apps/cli/dist/snap/bw_${{ env._PACKAGE_VERSION }}_amd64.snap
|
path: apps/cli/dist/snap/bw_${{ env._PACKAGE_VERSION }}_amd64.snap
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload snap checksum asset
|
- name: Upload snap checksum asset
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt
|
name: bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||||
path: apps/cli/dist/snap/bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt
|
path: apps/cli/dist/snap/bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||||
|
|
|
@ -174,61 +174,62 @@ jobs:
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
apps/desktop/desktop_native/napi/*.node
|
apps/desktop/desktop_native/napi/*.node
|
||||||
|
apps/desktop/desktop_native/dist/*
|
||||||
${{ env.RUNNER_TEMP }}/.cargo/registry
|
${{ env.RUNNER_TEMP }}/.cargo/registry
|
||||||
${{ env.RUNNER_TEMP }}/.cargo/git
|
${{ env.RUNNER_TEMP }}/.cargo/git
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||||
|
|
||||||
- name: Build Native Module
|
- name: Build Native Module
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
working-directory: apps/desktop/desktop_native/napi
|
working-directory: apps/desktop/desktop_native
|
||||||
env:
|
env:
|
||||||
PKG_CONFIG_ALLOW_CROSS: true
|
PKG_CONFIG_ALLOW_CROSS: true
|
||||||
PKG_CONFIG_ALL_STATIC: true
|
PKG_CONFIG_ALL_STATIC: true
|
||||||
TARGET: musl
|
TARGET: musl
|
||||||
run: |
|
run: |
|
||||||
rustup target add x86_64-unknown-linux-musl
|
rustup target add x86_64-unknown-linux-musl
|
||||||
npm run build:cross-platform
|
node build.js cross-platform
|
||||||
|
|
||||||
- name: Build application
|
- name: Build application
|
||||||
run: npm run dist:lin
|
run: npm run dist:lin
|
||||||
|
|
||||||
- name: Upload .deb artifact
|
- name: Upload .deb artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .rpm artifact
|
- name: Upload .rpm artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .freebsd artifact
|
- name: Upload .freebsd artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .snap artifact
|
- name: Upload .snap artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
|
name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
|
||||||
path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
|
path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .AppImage artifact
|
- name: Upload .AppImage artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload auto-update artifact
|
- name: Upload auto-update artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: ${{ needs.setup.outputs.release_channel }}-linux.yml
|
name: ${{ needs.setup.outputs.release_channel }}-linux.yml
|
||||||
path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml
|
path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml
|
||||||
|
@ -301,13 +302,15 @@ jobs:
|
||||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: apps/desktop/desktop_native/napi/*.node
|
path: |
|
||||||
|
apps/desktop/desktop_native/napi/*.node
|
||||||
|
apps/desktop/desktop_native/dist/*
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||||
|
|
||||||
- name: Build Native Module
|
- name: Build Native Module
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
working-directory: apps/desktop/desktop_native/napi
|
working-directory: apps/desktop/desktop_native
|
||||||
run: npm run build:cross-platform
|
run: node build.js cross-platform
|
||||||
|
|
||||||
- name: Build & Sign (dev)
|
- name: Build & Sign (dev)
|
||||||
env:
|
env:
|
||||||
|
@ -351,91 +354,91 @@ jobs:
|
||||||
-NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
-NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
||||||
|
|
||||||
- name: Upload portable exe artifact
|
- name: Upload portable exe artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
|
name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||||
path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
|
path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload installer exe artifact
|
- name: Upload installer exe artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
|
name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||||
path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
|
path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload appx ia32 artifact
|
- name: Upload appx ia32 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload store appx ia32 artifact
|
- name: Upload store appx ia32 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload NSIS ia32 artifact
|
- name: Upload NSIS ia32 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
|
name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
|
||||||
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
|
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload appx x64 artifact
|
- name: Upload appx x64 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload store appx x64 artifact
|
- name: Upload store appx x64 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload NSIS x64 artifact
|
- name: Upload NSIS x64 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
|
name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
|
||||||
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
|
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload appx ARM64 artifact
|
- name: Upload appx ARM64 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload store appx ARM64 artifact
|
- name: Upload store appx ARM64 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload NSIS ARM64 artifact
|
- name: Upload NSIS ARM64 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
||||||
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload nupkg artifact
|
- name: Upload nupkg artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
|
name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
|
||||||
path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
|
path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload auto-update artifact
|
- name: Upload auto-update artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: ${{ needs.setup.outputs.release_channel }}.yml
|
name: ${{ needs.setup.outputs.release_channel }}.yml
|
||||||
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml
|
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml
|
||||||
|
@ -584,13 +587,15 @@ jobs:
|
||||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: apps/desktop/desktop_native/napi/*.node
|
path: |
|
||||||
|
apps/desktop/desktop_native/napi/*.node
|
||||||
|
apps/desktop/desktop_native/dist/*
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||||
|
|
||||||
- name: Build Native Module
|
- name: Build Native Module
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
working-directory: apps/desktop/desktop_native/napi
|
working-directory: apps/desktop/desktop_native
|
||||||
run: npm run build:cross-platform
|
run: node build.js cross-platform
|
||||||
|
|
||||||
- name: Build application (dev)
|
- name: Build application (dev)
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
@ -748,13 +753,15 @@ jobs:
|
||||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: apps/desktop/desktop_native/napi/*.node
|
path: |
|
||||||
|
apps/desktop/desktop_native/napi/*.node
|
||||||
|
apps/desktop/desktop_native/dist/*
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||||
|
|
||||||
- name: Build Native Module
|
- name: Build Native Module
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
working-directory: apps/desktop/desktop_native/napi
|
working-directory: apps/desktop/desktop_native
|
||||||
run: npm run build:cross-platform
|
run: node build.js cross-platform
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||||
|
@ -792,28 +799,28 @@ jobs:
|
||||||
run: npm run pack:mac
|
run: npm run pack:mac
|
||||||
|
|
||||||
- name: Upload .zip artifact
|
- name: Upload .zip artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .dmg artifact
|
- name: Upload .dmg artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .dmg blockmap artifact
|
- name: Upload .dmg blockmap artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload auto-update artifact
|
- name: Upload auto-update artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: ${{ needs.setup.outputs.release_channel }}-mac.yml
|
name: ${{ needs.setup.outputs.release_channel }}-mac.yml
|
||||||
path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml
|
path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml
|
||||||
|
@ -965,13 +972,15 @@ jobs:
|
||||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: apps/desktop/desktop_native/napi/*.node
|
path: |
|
||||||
|
apps/desktop/desktop_native/napi/*.node
|
||||||
|
apps/desktop/desktop_native/dist/*
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||||
|
|
||||||
- name: Build Native Module
|
- name: Build Native Module
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
working-directory: apps/desktop/desktop_native/napi
|
working-directory: apps/desktop/desktop_native
|
||||||
run: npm run build:cross-platform
|
run: node build.js cross-platform
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||||
|
@ -1009,7 +1018,7 @@ jobs:
|
||||||
run: npm run pack:mac:mas
|
run: npm run pack:mac:mas
|
||||||
|
|
||||||
- name: Upload .pkg artifact
|
- name: Upload .pkg artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
|
||||||
path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
|
path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
|
||||||
|
@ -1168,13 +1177,15 @@ jobs:
|
||||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: apps/desktop/desktop_native/napi/*.node
|
path: |
|
||||||
|
apps/desktop/desktop_native/napi/*.node
|
||||||
|
apps/desktop/desktop_native/dist/*
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||||
|
|
||||||
- name: Build Native Module
|
- name: Build Native Module
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
working-directory: apps/desktop/desktop_native/napi
|
working-directory: apps/desktop/desktop_native
|
||||||
run: npm run build:cross-platform
|
run: node build.js cross-platform
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||||
|
@ -1215,7 +1226,7 @@ jobs:
|
||||||
zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app
|
zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app
|
||||||
|
|
||||||
- name: Upload masdev artifact
|
- name: Upload masdev artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip
|
||||||
path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip
|
path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip
|
||||||
|
@ -1248,7 +1259,7 @@ jobs:
|
||||||
secrets: "crowdin-api-token"
|
secrets: "crowdin-api-token"
|
||||||
|
|
||||||
- name: Upload Sources
|
- name: Upload Sources
|
||||||
uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0
|
uses: crowdin/github-action@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||||
|
|
|
@ -130,7 +130,7 @@ jobs:
|
||||||
run: zip -r web-${{ env._VERSION }}-${{ matrix.name }}.zip build
|
run: zip -r web-${{ env._VERSION }}-${{ matrix.name }}.zip build
|
||||||
|
|
||||||
- name: Upload ${{ matrix.name }} artifact
|
- name: Upload ${{ matrix.name }} artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: web-${{ env._VERSION }}-${{ matrix.name }}.zip
|
name: web-${{ env._VERSION }}-${{ matrix.name }}.zip
|
||||||
path: apps/web/web-${{ env._VERSION }}-${{ matrix.name }}.zip
|
path: apps/web/web-${{ env._VERSION }}-${{ matrix.name }}.zip
|
||||||
|
@ -270,7 +270,7 @@ jobs:
|
||||||
secrets: "crowdin-api-token"
|
secrets: "crowdin-api-token"
|
||||||
|
|
||||||
- name: Upload Sources
|
- name: Upload Sources
|
||||||
uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0
|
uses: crowdin/github-action@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||||
|
|
|
@ -159,42 +159,42 @@ jobs:
|
||||||
run: npm run dist:lin
|
run: npm run dist:lin
|
||||||
|
|
||||||
- name: Upload .deb artifact
|
- name: Upload .deb artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .rpm artifact
|
- name: Upload .rpm artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .freebsd artifact
|
- name: Upload .freebsd artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .snap artifact
|
- name: Upload .snap artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
|
name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
|
||||||
path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
|
path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .AppImage artifact
|
- name: Upload .AppImage artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload auto-update artifact
|
- name: Upload auto-update artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: ${{ needs.setup.outputs.release-channel }}-linux.yml
|
name: ${{ needs.setup.outputs.release-channel }}-linux.yml
|
||||||
path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-linux.yml
|
path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-linux.yml
|
||||||
|
@ -300,91 +300,91 @@ jobs:
|
||||||
-NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
-NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
||||||
|
|
||||||
- name: Upload portable exe artifact
|
- name: Upload portable exe artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
|
name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||||
path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
|
path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload installer exe artifact
|
- name: Upload installer exe artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
|
name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||||
path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
|
path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload appx ia32 artifact
|
- name: Upload appx ia32 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload store appx ia32 artifact
|
- name: Upload store appx ia32 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload NSIS ia32 artifact
|
- name: Upload NSIS ia32 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
|
name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
|
||||||
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
|
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload appx x64 artifact
|
- name: Upload appx x64 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload store appx x64 artifact
|
- name: Upload store appx x64 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload NSIS x64 artifact
|
- name: Upload NSIS x64 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
|
name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
|
||||||
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
|
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload appx ARM64 artifact
|
- name: Upload appx ARM64 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload store appx ARM64 artifact
|
- name: Upload store appx ARM64 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload NSIS ARM64 artifact
|
- name: Upload NSIS ARM64 artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
||||||
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload nupkg artifact
|
- name: Upload nupkg artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
|
name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
|
||||||
path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
|
path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload auto-update artifact
|
- name: Upload auto-update artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: ${{ needs.setup.outputs.release-channel }}.yml
|
name: ${{ needs.setup.outputs.release-channel }}.yml
|
||||||
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release-channel }}.yml
|
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release-channel }}.yml
|
||||||
|
@ -708,28 +708,28 @@ jobs:
|
||||||
run: npm run pack:mac
|
run: npm run pack:mac
|
||||||
|
|
||||||
- name: Upload .zip artifact
|
- name: Upload .zip artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .dmg artifact
|
- name: Upload .dmg artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .dmg blockmap artifact
|
- name: Upload .dmg blockmap artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
|
||||||
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
|
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload auto-update artifact
|
- name: Upload auto-update artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: ${{ needs.setup.outputs.release-channel }}-mac.yml
|
name: ${{ needs.setup.outputs.release-channel }}-mac.yml
|
||||||
path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-mac.yml
|
path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-mac.yml
|
||||||
|
@ -916,7 +916,7 @@ jobs:
|
||||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||||
|
|
||||||
- name: Upload .pkg artifact
|
- name: Upload .pkg artifact
|
||||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
|
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
|
||||||
path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
|
path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
|
||||||
|
|
|
@ -47,7 +47,7 @@ jobs:
|
||||||
--output-path . ${{ env.INCREMENTAL }}
|
--output-path . ${{ env.INCREMENTAL }}
|
||||||
|
|
||||||
- name: Upload Checkmarx results to GitHub
|
- name: Upload Checkmarx results to GitHub
|
||||||
uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5
|
uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
|
||||||
with:
|
with:
|
||||||
sarif_file: cx_result.sarif
|
sarif_file: cx_result.sarif
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ jobs:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Scan with SonarCloud
|
- name: Scan with SonarCloud
|
||||||
uses: sonarsource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92 # v2.3.0
|
uses: sonarsource/sonarcloud-github-action@eb211723266fe8e83102bac7361f0a05c3ac1d1b # v3.0.0
|
||||||
env:
|
env:
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"devFlags": {},
|
"devFlags": {},
|
||||||
"flags": {
|
"flags": {
|
||||||
"showPasswordless": true,
|
"showPasswordless": true,
|
||||||
"enableCipherKeyEncryption": true,
|
"enableCipherKeyEncryption": false,
|
||||||
"accountSwitching": false
|
"accountSwitching": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
},
|
},
|
||||||
"flags": {
|
"flags": {
|
||||||
"showPasswordless": true,
|
"showPasswordless": true,
|
||||||
"enableCipherKeyEncryption": true,
|
"enableCipherKeyEncryption": false,
|
||||||
"accountSwitching": true
|
"accountSwitching": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"flags": {
|
"flags": {
|
||||||
"enableCipherKeyEncryption": true,
|
"enableCipherKeyEncryption": false,
|
||||||
"accountSwitching": true
|
"accountSwitching": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@bitwarden/browser",
|
"name": "@bitwarden/browser",
|
||||||
"version": "2024.8.1",
|
"version": "2024.8.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env MANIFEST_VERSION=3 webpack",
|
"build": "cross-env MANIFEST_VERSION=3 webpack",
|
||||||
"build:mv2": "webpack",
|
"build:mv2": "webpack",
|
||||||
|
|
|
@ -3477,7 +3477,7 @@
|
||||||
"passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": {
|
"passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": {
|
||||||
"message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password."
|
"message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password."
|
||||||
},
|
},
|
||||||
"logInWithPasskey": {
|
"logInWithPasskeyQuestion": {
|
||||||
"message": "Log in with passkey?"
|
"message": "Log in with passkey?"
|
||||||
},
|
},
|
||||||
"passkeyAlreadyExists": {
|
"passkeyAlreadyExists": {
|
||||||
|
@ -3489,6 +3489,9 @@
|
||||||
"noMatchingPasskeyLogin": {
|
"noMatchingPasskeyLogin": {
|
||||||
"message": "You do not have a matching login for this site."
|
"message": "You do not have a matching login for this site."
|
||||||
},
|
},
|
||||||
|
"noMatchingLoginsForSite": {
|
||||||
|
"message": "No matching logins for this site"
|
||||||
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"message": "Confirm"
|
"message": "Confirm"
|
||||||
},
|
},
|
||||||
|
@ -3498,9 +3501,12 @@
|
||||||
"savePasskeyNewLogin": {
|
"savePasskeyNewLogin": {
|
||||||
"message": "Save passkey as new login"
|
"message": "Save passkey as new login"
|
||||||
},
|
},
|
||||||
"choosePasskey": {
|
"chooseCipherForPasskeySave": {
|
||||||
"message": "Choose a login to save this passkey to"
|
"message": "Choose a login to save this passkey to"
|
||||||
},
|
},
|
||||||
|
"chooseCipherForPasskeyAuth": {
|
||||||
|
"message": "Choose a passkey to log in with"
|
||||||
|
},
|
||||||
"passkeyItem": {
|
"passkeyItem": {
|
||||||
"message": "Passkey Item"
|
"message": "Passkey Item"
|
||||||
},
|
},
|
||||||
|
@ -4296,5 +4302,26 @@
|
||||||
},
|
},
|
||||||
"additionalContentAvailable": {
|
"additionalContentAvailable": {
|
||||||
"message": "Additional content is available"
|
"message": "Additional content is available"
|
||||||
|
},
|
||||||
|
"itemsInTrash": {
|
||||||
|
"message": "Items in trash"
|
||||||
|
},
|
||||||
|
"noItemsInTrash": {
|
||||||
|
"message": "No items in trash"
|
||||||
|
},
|
||||||
|
"noItemsInTrashDesc": {
|
||||||
|
"message": "Items you delete will appear here and be permanently deleted after 30 days"
|
||||||
|
},
|
||||||
|
"trashWarning": {
|
||||||
|
"message": "Items that have been in trash more than 30 days will automatically be deleted"
|
||||||
|
},
|
||||||
|
"restore": {
|
||||||
|
"message": "Restore"
|
||||||
|
},
|
||||||
|
"deleteForever": {
|
||||||
|
"message": "Delete forever"
|
||||||
|
},
|
||||||
|
"noEditPermissions": {
|
||||||
|
"message": "You don't have permission to edit this item"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router";
|
import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router";
|
||||||
import { Subject, filter, firstValueFrom, switchMap, takeUntil, tap } from "rxjs";
|
import { Subject, filter, switchMap, takeUntil, tap } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AnonLayoutComponent,
|
AnonLayoutComponent,
|
||||||
|
@ -9,7 +9,6 @@ import {
|
||||||
AnonLayoutWrapperDataService,
|
AnonLayoutWrapperDataService,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
|
||||||
import { Icon, IconModule } from "@bitwarden/components";
|
import { Icon, IconModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
|
@ -17,10 +16,7 @@ import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-heade
|
||||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
import { CurrentAccountComponent } from "../account-switching/current-account.component";
|
import { CurrentAccountComponent } from "../account-switching/current-account.component";
|
||||||
|
|
||||||
import {
|
import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon";
|
||||||
ExtensionBitwardenLogoPrimary,
|
|
||||||
ExtensionBitwardenLogoWhite,
|
|
||||||
} from "./extension-bitwarden-logo.icon";
|
|
||||||
|
|
||||||
export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData {
|
export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData {
|
||||||
showAcctSwitcher?: boolean;
|
showAcctSwitcher?: boolean;
|
||||||
|
@ -56,14 +52,13 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
|
||||||
protected maxWidth: "md" | "3xl";
|
protected maxWidth: "md" | "3xl";
|
||||||
|
|
||||||
protected theme: string;
|
protected theme: string;
|
||||||
protected logo: Icon;
|
protected logo = ExtensionBitwardenLogo;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||||
private themeStateService: ThemeStateService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
|
@ -73,14 +68,6 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
|
||||||
// Listen for page changes and update the page data appropriately
|
// Listen for page changes and update the page data appropriately
|
||||||
this.listenForPageDataChanges();
|
this.listenForPageDataChanges();
|
||||||
this.listenForServiceDataChanges();
|
this.listenForServiceDataChanges();
|
||||||
|
|
||||||
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
|
|
||||||
|
|
||||||
if (this.theme === "dark") {
|
|
||||||
this.logo = ExtensionBitwardenLogoWhite;
|
|
||||||
} else {
|
|
||||||
this.logo = ExtensionBitwardenLogoPrimary;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private listenForPageDataChanges() {
|
private listenForPageDataChanges() {
|
||||||
|
|
|
@ -22,8 +22,6 @@ import {
|
||||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { ButtonModule, I18nMockService } from "@bitwarden/components";
|
import { ButtonModule, I18nMockService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
@ -47,7 +45,6 @@ const decorators = (options: {
|
||||||
applicationVersion?: string;
|
applicationVersion?: string;
|
||||||
clientType?: ClientType;
|
clientType?: ClientType;
|
||||||
hostName?: string;
|
hostName?: string;
|
||||||
themeType?: ThemeType;
|
|
||||||
}) => {
|
}) => {
|
||||||
return [
|
return [
|
||||||
componentWrapperDecorator(
|
componentWrapperDecorator(
|
||||||
|
@ -120,12 +117,6 @@ const decorators = (options: {
|
||||||
getClientType: () => options.clientType || ClientType.Web,
|
getClientType: () => options.clientType || ClientType.Web,
|
||||||
} as Partial<PlatformUtilsService>,
|
} as Partial<PlatformUtilsService>,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: ThemeStateService,
|
|
||||||
useValue: {
|
|
||||||
selectedTheme$: of(options.themeType || ThemeType.Light),
|
|
||||||
} as Partial<ThemeStateService>,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: I18nService,
|
provide: I18nService,
|
||||||
useFactory: () => {
|
useFactory: () => {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -41,7 +41,7 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
const email = this.loginEmailService.getEmail();
|
const email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||||
const rememberEmail = this.loginEmailService.getRememberEmail();
|
const rememberEmail = this.loginEmailService.getRememberEmail();
|
||||||
|
|
||||||
if (email != null) {
|
if (email != null) {
|
||||||
|
@ -93,7 +93,7 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||||
async setLoginEmailValues() {
|
async setLoginEmailValues() {
|
||||||
// Note: Browser saves email settings here instead of the login component
|
// Note: Browser saves email settings here instead of the login component
|
||||||
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
|
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
|
||||||
this.loginEmailService.setEmail(this.formGroup.value.email);
|
await this.loginEmailService.setLoginEmail(this.formGroup.value.email);
|
||||||
await this.loginEmailService.saveEmailSettings();
|
await this.loginEmailService.saveEmailSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,7 @@
|
||||||
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }}
|
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<app-fido2-use-browser-link></app-fido2-use-browser-link>
|
<app-fido2-use-browser-link-v1></app-fido2-use-browser-link-v1>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</main>
|
</main>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, NgZone } from "@angular/core";
|
import { Component, NgZone, OnInit } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
@ -31,7 +31,7 @@ import { flagEnabled } from "../../platform/flags";
|
||||||
selector: "app-login",
|
selector: "app-login",
|
||||||
templateUrl: "login.component.html",
|
templateUrl: "login.component.html",
|
||||||
})
|
})
|
||||||
export class LoginComponent extends BaseLoginComponent {
|
export class LoginComponent extends BaseLoginComponent implements OnInit {
|
||||||
showPasswordless = false;
|
showPasswordless = false;
|
||||||
constructor(
|
constructor(
|
||||||
devicesApiService: DevicesApiServiceAbstraction,
|
devicesApiService: DevicesApiServiceAbstraction,
|
||||||
|
@ -83,13 +83,12 @@ export class LoginComponent extends BaseLoginComponent {
|
||||||
};
|
};
|
||||||
super.successRoute = "/tabs/vault";
|
super.successRoute = "/tabs/vault";
|
||||||
this.showPasswordless = flagEnabled("showPasswordless");
|
this.showPasswordless = flagEnabled("showPasswordless");
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
await super.ngOnInit();
|
||||||
if (this.showPasswordless) {
|
if (this.showPasswordless) {
|
||||||
this.formGroup.controls.email.setValue(this.loginEmailService.getEmail());
|
await this.validateEmail();
|
||||||
this.formGroup.controls.rememberEmail.setValue(this.loginEmailService.getRememberEmail());
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.validateEmail();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -139,7 +139,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||||
this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId),
|
this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId),
|
||||||
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
|
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
|
||||||
unlockCompleted: ({ message }) => this.unlockCompleted(message),
|
unlockCompleted: ({ message }) => this.unlockCompleted(message),
|
||||||
doFullSync: () => this.updateOverlayCiphers(true),
|
doFullSync: () => this.updateOverlayCiphers(),
|
||||||
addedCipher: () => this.updateOverlayCiphers(),
|
addedCipher: () => this.updateOverlayCiphers(),
|
||||||
addEditCipherSubmitted: () => this.updateOverlayCiphers(),
|
addEditCipherSubmitted: () => this.updateOverlayCiphers(),
|
||||||
editedCipher: () => this.updateOverlayCiphers(),
|
editedCipher: () => this.updateOverlayCiphers(),
|
||||||
|
@ -272,7 +272,10 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||||
this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error));
|
this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentTab) {
|
if (!currentTab || !currentTab.url?.startsWith("http")) {
|
||||||
|
if (updateAllCipherTypes) {
|
||||||
|
this.cardAndIdentityCiphers = null;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,21 @@ describe("FIDO2 page-script for manifest v2", () => {
|
||||||
let createdScriptElement: HTMLScriptElement;
|
let createdScriptElement: HTMLScriptElement;
|
||||||
jest.spyOn(window.document, "createElement");
|
jest.spyOn(window.document, "createElement");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
Object.defineProperty(window.document, "contentType", { value: "text/html", writable: true });
|
Object.defineProperty(window.document, "contentType", { value: "text/html", writable: true });
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
jest.clearAllTimers();
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
it("skips appending the `page-script.js` file if the document contentType is not `text/html`", () => {
|
it("skips appending the `page-script.js` file if the document contentType is not `text/html`", () => {
|
||||||
Object.defineProperty(window.document, "contentType", { value: "text/plain", writable: true });
|
Object.defineProperty(window.document, "contentType", { value: "text/plain", writable: true });
|
||||||
|
|
||||||
|
@ -19,7 +28,7 @@ describe("FIDO2 page-script for manifest v2", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("appends the `page-script.js` file to the document head when the contentType is `text/html`", () => {
|
it("appends the `page-script.js` file to the document head when the contentType is `text/html`", () => {
|
||||||
jest.spyOn(window.document.head, "insertBefore").mockImplementation((node) => {
|
jest.spyOn(window.document.head, "prepend").mockImplementation((node) => {
|
||||||
createdScriptElement = node as HTMLScriptElement;
|
createdScriptElement = node as HTMLScriptElement;
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
|
@ -28,16 +37,13 @@ describe("FIDO2 page-script for manifest v2", () => {
|
||||||
|
|
||||||
expect(window.document.createElement).toHaveBeenCalledWith("script");
|
expect(window.document.createElement).toHaveBeenCalledWith("script");
|
||||||
expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript);
|
expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript);
|
||||||
expect(window.document.head.insertBefore).toHaveBeenCalledWith(
|
expect(window.document.head.prepend).toHaveBeenCalledWith(expect.any(HTMLScriptElement));
|
||||||
expect.any(HTMLScriptElement),
|
|
||||||
window.document.head.firstChild,
|
|
||||||
);
|
|
||||||
expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`);
|
expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("appends the `page-script.js` file to the document element if the head is not available", () => {
|
it("appends the `page-script.js` file to the document element if the head is not available", () => {
|
||||||
window.document.documentElement.removeChild(window.document.head);
|
window.document.documentElement.removeChild(window.document.head);
|
||||||
jest.spyOn(window.document.documentElement, "insertBefore").mockImplementation((node) => {
|
jest.spyOn(window.document.documentElement, "prepend").mockImplementation((node) => {
|
||||||
createdScriptElement = node as HTMLScriptElement;
|
createdScriptElement = node as HTMLScriptElement;
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
|
@ -46,9 +52,8 @@ describe("FIDO2 page-script for manifest v2", () => {
|
||||||
|
|
||||||
expect(window.document.createElement).toHaveBeenCalledWith("script");
|
expect(window.document.createElement).toHaveBeenCalledWith("script");
|
||||||
expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript);
|
expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript);
|
||||||
expect(window.document.documentElement.insertBefore).toHaveBeenCalledWith(
|
expect(window.document.documentElement.prepend).toHaveBeenCalledWith(
|
||||||
expect.any(HTMLScriptElement),
|
expect.any(HTMLScriptElement),
|
||||||
window.document.documentElement.firstChild,
|
|
||||||
);
|
);
|
||||||
expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`);
|
expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`);
|
||||||
});
|
});
|
||||||
|
@ -63,6 +68,7 @@ describe("FIDO2 page-script for manifest v2", () => {
|
||||||
|
|
||||||
jest.spyOn(createdScriptElement, "remove");
|
jest.spyOn(createdScriptElement, "remove");
|
||||||
createdScriptElement.dispatchEvent(new Event("load"));
|
createdScriptElement.dispatchEvent(new Event("load"));
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
expect(createdScriptElement.remove).toHaveBeenCalled();
|
expect(createdScriptElement.remove).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,18 +2,20 @@
|
||||||
* This script handles injection of the FIDO2 override page script into the document.
|
* This script handles injection of the FIDO2 override page script into the document.
|
||||||
* This is required for manifest v2, but will be removed when we migrate fully to manifest v3.
|
* This is required for manifest v2, but will be removed when we migrate fully to manifest v3.
|
||||||
*/
|
*/
|
||||||
import { Fido2ContentScript } from "../enums/fido2-content-script.enum";
|
|
||||||
|
|
||||||
(function (globalContext) {
|
(function (globalContext) {
|
||||||
if (globalContext.document.contentType !== "text/html") {
|
if (globalContext.document.contentType !== "text/html") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const script = globalContext.document.createElement("script");
|
const script = globalContext.document.createElement("script");
|
||||||
script.src = chrome.runtime.getURL(Fido2ContentScript.PageScript);
|
script.src = chrome.runtime.getURL("content/fido2-page-script.js");
|
||||||
script.addEventListener("load", () => script.remove());
|
script.addEventListener("load", removeScriptOnLoad);
|
||||||
|
|
||||||
const scriptInsertionPoint =
|
const scriptInsertionPoint =
|
||||||
globalContext.document.head || globalContext.document.documentElement;
|
globalContext.document.head || globalContext.document.documentElement;
|
||||||
scriptInsertionPoint.insertBefore(script, scriptInsertionPoint.firstChild);
|
scriptInsertionPoint.prepend(script);
|
||||||
|
|
||||||
|
function removeScriptOnLoad() {
|
||||||
|
globalThis.setTimeout(() => script?.remove(), 5000);
|
||||||
|
}
|
||||||
})(globalThis);
|
})(globalThis);
|
||||||
|
|
|
@ -2,26 +2,35 @@
|
||||||
* This script handles injection of the FIDO2 override page script into the document.
|
* This script handles injection of the FIDO2 override page script into the document.
|
||||||
* This is required for manifest v2, but will be removed when we migrate fully to manifest v3.
|
* This is required for manifest v2, but will be removed when we migrate fully to manifest v3.
|
||||||
*/
|
*/
|
||||||
import { Fido2ContentScript } from "../enums/fido2-content-script.enum";
|
|
||||||
|
|
||||||
(function (globalContext) {
|
(function (globalContext) {
|
||||||
if (globalContext.document.contentType !== "text/html") {
|
if (globalContext.document.contentType !== "text/html") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (globalContext.document.readyState === "complete") {
|
const script = globalContext.document.createElement("script");
|
||||||
loadScript();
|
script.src = chrome.runtime.getURL("content/fido2-page-script.js");
|
||||||
|
script.addEventListener("load", removeScriptOnLoad);
|
||||||
|
|
||||||
|
// We are ensuring that the script injection is delayed in the event that we are loading
|
||||||
|
// within an iframe element. This prevents an issue with web mail clients that load content
|
||||||
|
// using ajax within iframes. In particular, Zimbra web mail client was observed to have this issue.
|
||||||
|
// @see https://github.com/bitwarden/clients/issues/9618
|
||||||
|
const delayScriptInjection =
|
||||||
|
globalContext.window.top !== globalContext.window &&
|
||||||
|
globalContext.document.readyState !== "complete";
|
||||||
|
if (delayScriptInjection) {
|
||||||
|
globalContext.document.addEventListener("DOMContentLoaded", injectScript);
|
||||||
} else {
|
} else {
|
||||||
globalContext.addEventListener("DOMContentLoaded", loadScript);
|
injectScript();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadScript() {
|
function injectScript() {
|
||||||
const script = globalContext.document.createElement("script");
|
|
||||||
script.src = chrome.runtime.getURL(Fido2ContentScript.PageScript);
|
|
||||||
script.addEventListener("load", () => script.remove());
|
|
||||||
|
|
||||||
const scriptInsertionPoint =
|
const scriptInsertionPoint =
|
||||||
globalContext.document.head || globalContext.document.documentElement;
|
globalContext.document.head || globalContext.document.documentElement;
|
||||||
scriptInsertionPoint.insertBefore(script, scriptInsertionPoint.firstChild);
|
scriptInsertionPoint.prepend(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeScriptOnLoad() {
|
||||||
|
globalThis.setTimeout(() => script?.remove(), 5000);
|
||||||
}
|
}
|
||||||
})(globalThis);
|
})(globalThis);
|
||||||
|
|
|
@ -18,7 +18,7 @@ export enum MessageType {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The params provided by the page-script are created in an insecure environemnt and
|
* The params provided by the page-script are created in an insecure environment and
|
||||||
* should not be trusted. This type is used to ensure that the content-script does not
|
* should not be trusted. This type is used to ensure that the content-script does not
|
||||||
* trust the `origin` or `sameOriginWithAncestors` params.
|
* trust the `origin` or `sameOriginWithAncestors` params.
|
||||||
*/
|
*/
|
||||||
|
@ -38,7 +38,7 @@ export type CredentialCreationResponse = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The params provided by the page-script are created in an insecure environemnt and
|
* The params provided by the page-script are created in an insecure environment and
|
||||||
* should not be trusted. This type is used to ensure that the content-script does not
|
* should not be trusted. This type is used to ensure that the content-script does not
|
||||||
* trust the `origin` or `sameOriginWithAncestors` params.
|
* trust the `origin` or `sameOriginWithAncestors` params.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
appA11yTitle="{{ cipher.name }} {{ cipher.subTitle }}"
|
||||||
|
class="virtual-scroll-item"
|
||||||
|
[ngClass]="{ 'override-last': !last }"
|
||||||
|
>
|
||||||
|
<div class="box-content-row box-content-row-flex">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
(click)="selectCipher(cipher)"
|
||||||
|
tabindex="0"
|
||||||
|
appStopClick
|
||||||
|
title="{{ title }} - {{ cipher.name }}"
|
||||||
|
[ngClass]="{ 'row-main': true, 'row-selected': isSelected && !isSearching }"
|
||||||
|
>
|
||||||
|
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
||||||
|
<div class="row-main-content">
|
||||||
|
<span class="text">
|
||||||
|
<span class="truncate-box">
|
||||||
|
<span class="truncate">{{ cipher.name }}</span>
|
||||||
|
<ng-container *ngIf="cipher.organizationId">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-collection text-muted"
|
||||||
|
title="{{ 'shared' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||||
|
</ng-container>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="detail" *ngIf="getSubName(cipher)">{{ getSubName(cipher) }}</span>
|
||||||
|
<span class="detail" *ngIf="cipher.subTitle">{{ cipher.subTitle }}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core";
|
||||||
|
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-fido2-cipher-row-v1",
|
||||||
|
templateUrl: "fido2-cipher-row-v1.component.html",
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class Fido2CipherRowV1Component {
|
||||||
|
@Output() onSelected = new EventEmitter<CipherView>();
|
||||||
|
@Input() cipher: CipherView;
|
||||||
|
@Input() last: boolean;
|
||||||
|
@Input() title: string;
|
||||||
|
@Input() isSearching: boolean;
|
||||||
|
@Input() isSelected: boolean;
|
||||||
|
|
||||||
|
protected selectCipher(c: CipherView) {
|
||||||
|
this.onSelected.emit(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a subname for the cipher.
|
||||||
|
* If this has a FIDO2 credential, and the cipher.name is different from the FIDO2 credential's rpId, return the rpId.
|
||||||
|
* @param c Cipher
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected getSubName(c: CipherView): string | null {
|
||||||
|
const fido2Credentials = c.login?.fido2Credentials;
|
||||||
|
|
||||||
|
if (!fido2Credentials || fido2Credentials.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [fido2Credential] = fido2Credentials;
|
||||||
|
|
||||||
|
return c.name !== fido2Credential.rpId ? fido2Credential.rpId : null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,36 +1,21 @@
|
||||||
<div
|
<bit-item>
|
||||||
role="group"
|
<button
|
||||||
appA11yTitle="{{ cipher.name }} {{ cipher.subTitle }}"
|
(click)="selectCipher(cipher)"
|
||||||
class="virtual-scroll-item"
|
appA11yTitle="{{ title }} - {{ cipher.name }}"
|
||||||
[ngClass]="{ 'override-last': !last }"
|
bit-item-content
|
||||||
>
|
tabindex="0"
|
||||||
<div class="box-content-row box-content-row-flex">
|
type="button"
|
||||||
<button
|
>
|
||||||
type="button"
|
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||||
(click)="selectCipher(cipher)"
|
<span data-testid="item-name">
|
||||||
tabindex="0"
|
{{ cipher.name }}
|
||||||
appStopClick
|
<i
|
||||||
title="{{ title }} - {{ cipher.name }}"
|
*ngIf="cipher.organizationId"
|
||||||
[ngClass]="{ 'row-main': true, 'row-selected': isSelected && !isSearching }"
|
[appA11yTitle]="'shared' | i18n"
|
||||||
>
|
class="bwi bwi-collection text-muted"
|
||||||
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
></i>
|
||||||
<div class="row-main-content">
|
</span>
|
||||||
<span class="text">
|
<span class="detail" *ngIf="getSubName(cipher)">{{ getSubName(cipher) }}</span>
|
||||||
<span class="truncate-box">
|
<span *ngIf="cipher.subTitle" slot="secondary">{{ cipher.subTitle }}</span>
|
||||||
<span class="truncate">{{ cipher.name }}</span>
|
</button>
|
||||||
<ng-container *ngIf="cipher.organizationId">
|
</bit-item>
|
||||||
<i
|
|
||||||
class="bwi bwi-collection text-muted"
|
|
||||||
title="{{ 'shared' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
|
||||||
</ng-container>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span class="detail" *ngIf="getSubName(cipher)">{{ getSubName(cipher) }}</span>
|
|
||||||
<span class="detail" *ngIf="cipher.subTitle">{{ cipher.subTitle }}</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -1,19 +1,40 @@
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core";
|
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import {
|
||||||
|
BadgeModule,
|
||||||
|
ButtonModule,
|
||||||
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
TypographyModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-fido2-cipher-row",
|
selector: "app-fido2-cipher-row",
|
||||||
templateUrl: "fido2-cipher-row.component.html",
|
templateUrl: "fido2-cipher-row.component.html",
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
BadgeModule,
|
||||||
|
ButtonModule,
|
||||||
|
CommonModule,
|
||||||
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
JslibModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
TypographyModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class Fido2CipherRowComponent {
|
export class Fido2CipherRowComponent {
|
||||||
@Output() onSelected = new EventEmitter<CipherView>();
|
@Output() onSelected = new EventEmitter<CipherView>();
|
||||||
@Input() cipher: CipherView;
|
@Input() cipher: CipherView;
|
||||||
@Input() last: boolean;
|
@Input() last: boolean;
|
||||||
@Input() title: string;
|
@Input() title: string;
|
||||||
@Input() isSearching: boolean;
|
|
||||||
@Input() isSelected: boolean;
|
|
||||||
|
|
||||||
protected selectCipher(c: CipherView) {
|
protected selectCipher(c: CipherView) {
|
||||||
this.onSelected.emit(c);
|
this.onSelected.emit(c);
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
<ng-container *ngIf="(fido2PopoutSessionData$ | async).fallbackSupported">
|
||||||
|
<div class="useBrowserlink">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
(click)="toggle()"
|
||||||
|
cdkOverlayOrigin
|
||||||
|
#trigger="cdkOverlayOrigin"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
aria-controls="cdk-overlay-container"
|
||||||
|
>
|
||||||
|
<span class="text-primary">
|
||||||
|
{{ "useDeviceOrHardwareKey" | i18n }}
|
||||||
|
</span>
|
||||||
|
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template
|
||||||
|
cdkConnectedOverlay
|
||||||
|
[cdkConnectedOverlayOrigin]="trigger"
|
||||||
|
[cdkConnectedOverlayOpen]="isOpen"
|
||||||
|
[cdkConnectedOverlayPositions]="overlayPosition"
|
||||||
|
[cdkConnectedOverlayHasBackdrop]="true"
|
||||||
|
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
|
||||||
|
(backdropClick)="isOpen = false"
|
||||||
|
(detach)="close()"
|
||||||
|
>
|
||||||
|
<div class="box-content">
|
||||||
|
<div
|
||||||
|
class="fido2-browser-selector-dropdown"
|
||||||
|
[@transformPanel]="'open'"
|
||||||
|
cdkTrapFocus
|
||||||
|
cdkTrapFocusAutoCapture
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
<button type="button" class="fido2-browser-selector-dropdown-item" (click)="abort(false)">
|
||||||
|
<span>{{ "justOnce" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
<br />
|
||||||
|
<button type="button" class="fido2-browser-selector-dropdown-item" (click)="abort()">
|
||||||
|
<span>{{ "alwaysForThisSite" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
*ngIf="showOverlay"
|
||||||
|
class="tw-absolute tw-w-full tw-h-full tw-bg-background-alt tw-inset-0 tw-bg-opacity-80 tw-z-50"
|
||||||
|
></div>
|
||||||
|
</ng-container>
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||||
|
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
|
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
|
import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data";
|
||||||
|
import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-fido2-use-browser-link-v1",
|
||||||
|
templateUrl: "fido2-use-browser-link-v1.component.html",
|
||||||
|
animations: [
|
||||||
|
trigger("transformPanel", [
|
||||||
|
state(
|
||||||
|
"void",
|
||||||
|
style({
|
||||||
|
opacity: 0,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
transition(
|
||||||
|
"void => open",
|
||||||
|
animate(
|
||||||
|
"100ms linear",
|
||||||
|
style({
|
||||||
|
opacity: 1,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class Fido2UseBrowserLinkV1Component {
|
||||||
|
showOverlay = false;
|
||||||
|
isOpen = false;
|
||||||
|
overlayPosition: ConnectedPosition[] = [
|
||||||
|
{
|
||||||
|
originX: "start",
|
||||||
|
originY: "bottom",
|
||||||
|
overlayX: "start",
|
||||||
|
overlayY: "top",
|
||||||
|
offsetY: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
protected fido2PopoutSessionData$ = fido2PopoutSessionData$();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private domainSettingsService: DomainSettingsService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.isOpen = !this.isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aborts the current FIDO2 session and fallsback to the browser.
|
||||||
|
* @param excludeDomain - Identifies if the domain should be excluded from future FIDO2 prompts.
|
||||||
|
*/
|
||||||
|
protected async abort(excludeDomain = true) {
|
||||||
|
this.close();
|
||||||
|
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||||
|
|
||||||
|
if (!excludeDomain) {
|
||||||
|
this.abortSession(sessionData.sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Show overlay to prevent the user from interacting with the page.
|
||||||
|
this.showOverlay = true;
|
||||||
|
await this.handleDomainExclusion(sessionData.senderUrl);
|
||||||
|
// Give the user a chance to see the toast before closing the popout.
|
||||||
|
await Utils.delay(2000);
|
||||||
|
this.abortSession(sessionData.sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excludes the domain from future FIDO2 prompts.
|
||||||
|
* @param uri - The domain uri to exclude from future FIDO2 prompts.
|
||||||
|
*/
|
||||||
|
private async handleDomainExclusion(uri: string) {
|
||||||
|
const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
|
||||||
|
|
||||||
|
const validDomain = Utils.getHostname(uri);
|
||||||
|
const savedDomains: NeverDomains = {
|
||||||
|
...existingDomains,
|
||||||
|
};
|
||||||
|
savedDomains[validDomain] = null;
|
||||||
|
|
||||||
|
await this.domainSettingsService.setNeverDomains(savedDomains);
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("domainAddedToExcludedDomains", validDomain),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private abortSession(sessionId: string) {
|
||||||
|
BrowserFido2UserInterfaceSession.abortPopout(sessionId, true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,11 @@
|
||||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
import { A11yModule } from "@angular/cdk/a11y";
|
||||||
|
import { ConnectedPosition, CdkOverlayOrigin, CdkConnectedOverlay } from "@angular/cdk/overlay";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
@ -15,6 +18,8 @@ import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-f
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-fido2-use-browser-link",
|
selector: "app-fido2-use-browser-link",
|
||||||
templateUrl: "fido2-use-browser-link.component.html",
|
templateUrl: "fido2-use-browser-link.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [A11yModule, CdkConnectedOverlay, CdkOverlayOrigin, CommonModule, JslibModule],
|
||||||
animations: [
|
animations: [
|
||||||
trigger("transformPanel", [
|
trigger("transformPanel", [
|
||||||
state(
|
state(
|
||||||
|
@ -90,11 +95,11 @@ export class Fido2UseBrowserLinkComponent {
|
||||||
* @param uri - The domain uri to exclude from future FIDO2 prompts.
|
* @param uri - The domain uri to exclude from future FIDO2 prompts.
|
||||||
*/
|
*/
|
||||||
private async handleDomainExclusion(uri: string) {
|
private async handleDomainExclusion(uri: string) {
|
||||||
const exisitingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
|
const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
|
||||||
|
|
||||||
const validDomain = Utils.getHostname(uri);
|
const validDomain = Utils.getHostname(uri);
|
||||||
const savedDomains: NeverDomains = {
|
const savedDomains: NeverDomains = {
|
||||||
...exisitingDomains,
|
...existingDomains,
|
||||||
};
|
};
|
||||||
savedDomains[validDomain] = null;
|
savedDomains[validDomain] = null;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
<ng-container *ngIf="data$ | async as data">
|
||||||
|
<div class="auth-wrapper">
|
||||||
|
<div class="auth-header">
|
||||||
|
<div class="left">
|
||||||
|
<ng-container *ngIf="data.message.type != BrowserFido2MessageTypes.PickCredentialRequest">
|
||||||
|
<div class="logo">
|
||||||
|
<i class="bwi bwi-shield"></i>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="data.message.type === BrowserFido2MessageTypes.PickCredentialRequest">
|
||||||
|
<div class="logo">
|
||||||
|
<i class="bwi bwi-shield"></i><span><strong>bit</strong>warden</span>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<ng-container
|
||||||
|
*ngIf="data.message.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest"
|
||||||
|
>
|
||||||
|
<div class="search">
|
||||||
|
<input
|
||||||
|
type="{{ searchTypeSearch ? 'search' : 'text' }}"
|
||||||
|
placeholder="{{ 'searchVault' | i18n }}"
|
||||||
|
id="search"
|
||||||
|
[(ngModel)]="searchText"
|
||||||
|
(input)="search()"
|
||||||
|
autocomplete="off"
|
||||||
|
appAutofocus
|
||||||
|
/>
|
||||||
|
<i class="bwi bwi-search" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
|
||||||
|
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container>
|
||||||
|
<ng-container
|
||||||
|
*ngIf="
|
||||||
|
data.message.type === BrowserFido2MessageTypes.PickCredentialRequest ||
|
||||||
|
data.message.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="auth-flow">
|
||||||
|
<p class="subtitle" appA11yTitle="{{ subtitleText | i18n }}">
|
||||||
|
{{ subtitleText | i18n }}
|
||||||
|
</p>
|
||||||
|
<!-- Display when ciphers exist -->
|
||||||
|
<ng-container *ngIf="displayedCiphers.length > 0">
|
||||||
|
<div class="box list">
|
||||||
|
<div class="box-content">
|
||||||
|
<app-fido2-cipher-row-v1
|
||||||
|
*ngFor="let cipherItem of displayedCiphers"
|
||||||
|
[cipher]="cipherItem"
|
||||||
|
[isSearching]="searchPending"
|
||||||
|
title="{{ 'passkeyItem' | i18n }}"
|
||||||
|
(onSelected)="selectedPasskey($event)"
|
||||||
|
[isSelected]="cipher === cipherItem"
|
||||||
|
></app-fido2-cipher-row-v1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
(click)="submit()"
|
||||||
|
class="btn primary block"
|
||||||
|
appA11yTitle="{{ credentialText | i18n }}"
|
||||||
|
>
|
||||||
|
<span [hidden]="loading">
|
||||||
|
{{ credentialText | i18n }}
|
||||||
|
</span>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-lg bwi-spin"
|
||||||
|
[hidden]="!loading"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="!displayedCiphers.length">
|
||||||
|
<div class="box">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
(click)="saveNewLogin()"
|
||||||
|
class="btn primary block"
|
||||||
|
appA11yTitle="{{ 'savePasskeyNewLogin' | i18n }}"
|
||||||
|
>
|
||||||
|
<span [hidden]="loading">
|
||||||
|
{{ "savePasskeyNewLogin" | i18n }}
|
||||||
|
</span>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-lg bwi-spin"
|
||||||
|
[hidden]="!loading"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container
|
||||||
|
*ngIf="data.message.type === BrowserFido2MessageTypes.InformExcludedCredentialRequest"
|
||||||
|
>
|
||||||
|
<div class="auth-flow">
|
||||||
|
<p class="subtitle">{{ "passkeyAlreadyExists" | i18n }}</p>
|
||||||
|
<div class="box list">
|
||||||
|
<div class="box-content">
|
||||||
|
<app-fido2-cipher-row-v1
|
||||||
|
*ngFor="let cipherItem of displayedCiphers"
|
||||||
|
[cipher]="cipherItem"
|
||||||
|
title="{{ 'passkeyItem' | i18n }}"
|
||||||
|
(onSelected)="selectedPasskey($event)"
|
||||||
|
[isSelected]="cipher === cipherItem"
|
||||||
|
></app-fido2-cipher-row-v1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn primary block" (click)="viewPasskey()">
|
||||||
|
<span [hidden]="loading">{{ "viewItem" | i18n }}</span>
|
||||||
|
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container
|
||||||
|
*ngIf="data.message.type === BrowserFido2MessageTypes.InformCredentialNotFoundRequest"
|
||||||
|
>
|
||||||
|
<div class="auth-flow">
|
||||||
|
<p class="subtitle">{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn primary block" (click)="abort(false)">
|
||||||
|
<span [hidden]="loading">{{ "close" | i18n }}</span>
|
||||||
|
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<app-fido2-use-browser-link-v1></app-fido2-use-browser-link-v1>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
|
@ -0,0 +1,443 @@
|
||||||
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
combineLatest,
|
||||||
|
concatMap,
|
||||||
|
filter,
|
||||||
|
firstValueFrom,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
Subject,
|
||||||
|
take,
|
||||||
|
takeUntil,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
||||||
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
|
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
|
||||||
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||||
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||||
|
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service";
|
||||||
|
import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window";
|
||||||
|
import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service";
|
||||||
|
import {
|
||||||
|
BrowserFido2Message,
|
||||||
|
BrowserFido2UserInterfaceSession,
|
||||||
|
BrowserFido2MessageTypes,
|
||||||
|
} from "../../fido2/services/browser-fido2-user-interface.service";
|
||||||
|
|
||||||
|
interface ViewData {
|
||||||
|
message: BrowserFido2Message;
|
||||||
|
fallbackSupported: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-fido2-v1",
|
||||||
|
templateUrl: "fido2-v1.component.html",
|
||||||
|
styleUrls: [],
|
||||||
|
})
|
||||||
|
export class Fido2V1Component implements OnInit, OnDestroy {
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private hasSearched = false;
|
||||||
|
|
||||||
|
protected cipher: CipherView;
|
||||||
|
protected searchTypeSearch = false;
|
||||||
|
protected searchPending = false;
|
||||||
|
protected searchText: string;
|
||||||
|
protected url: string;
|
||||||
|
protected hostname: string;
|
||||||
|
protected data$: Observable<ViewData>;
|
||||||
|
protected sessionId?: string;
|
||||||
|
protected senderTabId?: string;
|
||||||
|
protected ciphers?: CipherView[] = [];
|
||||||
|
protected displayedCiphers?: CipherView[] = [];
|
||||||
|
protected loading = false;
|
||||||
|
protected subtitleText: string;
|
||||||
|
protected credentialText: string;
|
||||||
|
protected BrowserFido2MessageTypes = BrowserFido2MessageTypes;
|
||||||
|
|
||||||
|
private message$ = new BehaviorSubject<BrowserFido2Message>(null);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private activatedRoute: ActivatedRoute,
|
||||||
|
private cipherService: CipherService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private domainSettingsService: DomainSettingsService,
|
||||||
|
private searchService: SearchService,
|
||||||
|
private logService: LogService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private browserMessagingApi: ZonedMessageListenerService,
|
||||||
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
|
private fido2UserVerificationService: Fido2UserVerificationService,
|
||||||
|
private accountService: AccountService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
||||||
|
|
||||||
|
const queryParams$ = this.activatedRoute.queryParamMap.pipe(
|
||||||
|
take(1),
|
||||||
|
map((queryParamMap) => ({
|
||||||
|
sessionId: queryParamMap.get("sessionId"),
|
||||||
|
senderTabId: queryParamMap.get("senderTabId"),
|
||||||
|
senderUrl: queryParamMap.get("senderUrl"),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
queryParams$,
|
||||||
|
this.browserMessagingApi.messageListener$() as Observable<BrowserFido2Message>,
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
concatMap(async ([queryParams, message]) => {
|
||||||
|
this.sessionId = queryParams.sessionId;
|
||||||
|
this.senderTabId = queryParams.senderTabId;
|
||||||
|
this.url = queryParams.senderUrl;
|
||||||
|
// For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
|
||||||
|
if (
|
||||||
|
message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest &&
|
||||||
|
message.sessionId !== queryParams.sessionId
|
||||||
|
) {
|
||||||
|
this.abort(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore messages that don't belong to the current session.
|
||||||
|
if (message.sessionId !== queryParams.sessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === BrowserFido2MessageTypes.AbortRequest) {
|
||||||
|
this.abort(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}),
|
||||||
|
filter((message) => !!message),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe((message) => {
|
||||||
|
this.message$.next(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.data$ = this.message$.pipe(
|
||||||
|
filter((message) => message != undefined),
|
||||||
|
concatMap(async (message) => {
|
||||||
|
switch (message.type) {
|
||||||
|
case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: {
|
||||||
|
const equivalentDomains = await firstValueFrom(
|
||||||
|
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
||||||
|
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
|
||||||
|
);
|
||||||
|
this.displayedCiphers = this.ciphers.filter(
|
||||||
|
(cipher) =>
|
||||||
|
cipher.login.matchesUri(this.url, equivalentDomains) &&
|
||||||
|
this.hasNoOtherPasskeys(cipher, message.userHandle),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.displayedCiphers.length > 0) {
|
||||||
|
this.selectedPasskey(this.displayedCiphers[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case BrowserFido2MessageTypes.PickCredentialRequest: {
|
||||||
|
const activeUserId = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.ciphers = await Promise.all(
|
||||||
|
message.cipherIds.map(async (cipherId) => {
|
||||||
|
const cipher = await this.cipherService.get(cipherId);
|
||||||
|
return cipher.decrypt(
|
||||||
|
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.displayedCiphers = [...this.ciphers];
|
||||||
|
if (this.displayedCiphers.length > 0) {
|
||||||
|
this.selectedPasskey(this.displayedCiphers[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case BrowserFido2MessageTypes.InformExcludedCredentialRequest: {
|
||||||
|
const activeUserId = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.ciphers = await Promise.all(
|
||||||
|
message.existingCipherIds.map(async (cipherId) => {
|
||||||
|
const cipher = await this.cipherService.get(cipherId);
|
||||||
|
return cipher.decrypt(
|
||||||
|
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.displayedCiphers = [...this.ciphers];
|
||||||
|
|
||||||
|
if (this.displayedCiphers.length > 0) {
|
||||||
|
this.selectedPasskey(this.displayedCiphers[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subtitleText =
|
||||||
|
this.displayedCiphers.length > 0
|
||||||
|
? this.getCredentialSubTitleText(message.type)
|
||||||
|
: "noMatchingPasskeyLogin";
|
||||||
|
|
||||||
|
this.credentialText = this.getCredentialButtonText(message.type);
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
fallbackSupported: "fallbackSupported" in message && message.fallbackSupported,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
);
|
||||||
|
|
||||||
|
queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => {
|
||||||
|
this.send({
|
||||||
|
sessionId: queryParams.sessionId,
|
||||||
|
type: BrowserFido2MessageTypes.ConnectResponse,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async submit() {
|
||||||
|
const data = this.message$.value;
|
||||||
|
if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) {
|
||||||
|
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
|
||||||
|
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||||
|
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
|
||||||
|
|
||||||
|
this.send({
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
cipherId: this.cipher.id,
|
||||||
|
type: BrowserFido2MessageTypes.PickCredentialResponse,
|
||||||
|
userVerified,
|
||||||
|
});
|
||||||
|
} else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||||
|
if (this.cipher.login.hasFido2Credentials) {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "overwritePasskey" },
|
||||||
|
content: { key: "overwritePasskeyAlert" },
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
|
||||||
|
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||||
|
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
|
||||||
|
|
||||||
|
this.send({
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
cipherId: this.cipher.id,
|
||||||
|
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
|
||||||
|
userVerified,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async saveNewLogin() {
|
||||||
|
const data = this.message$.value;
|
||||||
|
if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||||
|
const name = data.credentialName || data.rpId;
|
||||||
|
// TODO: Revert to check for user verification once user verification for passkeys is approved for production.
|
||||||
|
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||||
|
await this.createNewCipher(name, data.userName);
|
||||||
|
|
||||||
|
// We are bypassing user verification pending approval.
|
||||||
|
this.send({
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
cipherId: this.cipher?.id,
|
||||||
|
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
|
||||||
|
userVerified: data.userVerification,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCredentialSubTitleText(messageType: string): string {
|
||||||
|
return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest
|
||||||
|
? "chooseCipherForPasskeySave"
|
||||||
|
: "logInWithPasskeyQuestion";
|
||||||
|
}
|
||||||
|
|
||||||
|
getCredentialButtonText(messageType: string): string {
|
||||||
|
return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest
|
||||||
|
? "savePasskey"
|
||||||
|
: "confirm";
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedPasskey(item: CipherView) {
|
||||||
|
this.cipher = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewPasskey() {
|
||||||
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this.router.navigate(["/view-cipher"], {
|
||||||
|
queryParams: {
|
||||||
|
cipherId: this.cipher.id,
|
||||||
|
uilocation: "popout",
|
||||||
|
senderTabId: this.senderTabId,
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addCipher() {
|
||||||
|
const data = this.message$.value;
|
||||||
|
|
||||||
|
if (data?.type !== BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this.router.navigate(["/add-cipher"], {
|
||||||
|
queryParams: {
|
||||||
|
name: data.credentialName || data.rpId,
|
||||||
|
uri: this.url,
|
||||||
|
type: CipherType.Login.toString(),
|
||||||
|
uilocation: "popout",
|
||||||
|
username: data.userName,
|
||||||
|
senderTabId: this.senderTabId,
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
userVerification: data.userVerification,
|
||||||
|
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async search() {
|
||||||
|
this.hasSearched = await this.searchService.isSearchable(this.searchText);
|
||||||
|
this.searchPending = true;
|
||||||
|
if (this.hasSearched) {
|
||||||
|
this.displayedCiphers = await this.searchService.searchCiphers(
|
||||||
|
this.searchText,
|
||||||
|
null,
|
||||||
|
this.ciphers,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const equivalentDomains = await firstValueFrom(
|
||||||
|
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
||||||
|
);
|
||||||
|
this.displayedCiphers = this.ciphers.filter((cipher) =>
|
||||||
|
cipher.login.matchesUri(this.url, equivalentDomains),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.searchPending = false;
|
||||||
|
this.selectedPasskey(this.displayedCiphers[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(fallback: boolean) {
|
||||||
|
this.unload(fallback);
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
unload(fallback = false) {
|
||||||
|
this.send({
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
type: BrowserFido2MessageTypes.AbortResponse,
|
||||||
|
fallbackRequested: fallback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildCipher(name: string, username: string) {
|
||||||
|
this.cipher = new CipherView();
|
||||||
|
this.cipher.name = name;
|
||||||
|
|
||||||
|
this.cipher.type = CipherType.Login;
|
||||||
|
this.cipher.login = new LoginView();
|
||||||
|
this.cipher.login.username = username;
|
||||||
|
this.cipher.login.uris = [new LoginUriView()];
|
||||||
|
this.cipher.login.uris[0].uri = this.url;
|
||||||
|
this.cipher.card = new CardView();
|
||||||
|
this.cipher.identity = new IdentityView();
|
||||||
|
this.cipher.secureNote = new SecureNoteView();
|
||||||
|
this.cipher.secureNote.type = SecureNoteType.Generic;
|
||||||
|
this.cipher.reprompt = CipherRepromptType.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createNewCipher(name: string, username: string) {
|
||||||
|
const activeUserId = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.buildCipher(name, username);
|
||||||
|
const cipher = await this.cipherService.encrypt(this.cipher, activeUserId);
|
||||||
|
try {
|
||||||
|
await this.cipherService.createWithServer(cipher);
|
||||||
|
this.cipher.id = cipher.id;
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production.
|
||||||
|
private async handleUserVerification(
|
||||||
|
userVerificationRequested: boolean,
|
||||||
|
cipher: CipherView,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const masterPasswordRepromptRequired = cipher && cipher.reprompt !== 0;
|
||||||
|
|
||||||
|
if (masterPasswordRepromptRequired) {
|
||||||
|
return await this.passwordRepromptService.showPasswordPrompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
return userVerificationRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
private send(msg: BrowserFido2Message) {
|
||||||
|
BrowserFido2UserInterfaceSession.sendMessage({
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
...msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle
|
||||||
|
* @param userHandle
|
||||||
|
*/
|
||||||
|
private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean {
|
||||||
|
if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,136 +1,134 @@
|
||||||
<ng-container *ngIf="data$ | async as data">
|
<popup-page *ngIf="data$ | async as data">
|
||||||
<div class="auth-wrapper">
|
<popup-header
|
||||||
<div class="auth-header">
|
slot="header"
|
||||||
<div class="left">
|
pageTitle="{{
|
||||||
<ng-container *ngIf="data.message.type != 'PickCredentialRequest'">
|
(passkeyAction === PasskeyActions.Register ? 'savePasskey' : 'logInWithPasskeyQuestion')
|
||||||
<div class="logo">
|
| i18n
|
||||||
<i class="bwi bwi-shield"></i>
|
}}"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
*ngIf="showNewPasskeyButton"
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
type="button"
|
||||||
|
(click)="addCipher()"
|
||||||
|
slot="end"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
|
||||||
|
{{ "new" | i18n }}
|
||||||
|
</button>
|
||||||
|
</popup-header>
|
||||||
|
|
||||||
|
<div class="tw-p-2">
|
||||||
|
<bit-section *ngIf="passkeyAction === PasskeyActions.Register">
|
||||||
|
<bit-search
|
||||||
|
appAutofocus
|
||||||
|
autocomplete="off"
|
||||||
|
id="search"
|
||||||
|
placeholder="{{ 'searchVault' | i18n }}"
|
||||||
|
(ngModelChange)="search()"
|
||||||
|
[(ngModel)]="searchText"
|
||||||
|
></bit-search>
|
||||||
|
</bit-section>
|
||||||
|
|
||||||
|
<!-- Display when adding a new passkey -->
|
||||||
|
<bit-section *ngIf="data.message.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest">
|
||||||
|
<!-- Display when matching ciphers (i.e. same domain, no passkeys) exist -->
|
||||||
|
<ng-container *ngIf="displayedCiphers.length > 0">
|
||||||
|
<bit-section-header>
|
||||||
|
<h2 bitTypography="h6">{{ "chooseCipherForPasskeySave" | i18n }}</h2>
|
||||||
|
</bit-section-header>
|
||||||
|
<app-fido2-cipher-row
|
||||||
|
*ngFor="let cipherItem of displayedCiphers"
|
||||||
|
[cipher]="cipherItem"
|
||||||
|
title="{{ 'passkeyItem' | i18n }}"
|
||||||
|
(onSelected)="handleCipherItemSelect($event)"
|
||||||
|
></app-fido2-cipher-row>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Display when no matching ciphers exist -->
|
||||||
|
<ng-container *ngIf="!displayedCiphers.length">
|
||||||
|
<bit-no-items class="tw-text-main" [icon]="noResultsIcon">
|
||||||
|
<ng-container slot="title">{{ "noMatchingLoginsForSite" | i18n }}</ng-container>
|
||||||
|
<ng-container slot="description">Search or save passkey as new login</ng-container>
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
slot="button"
|
||||||
|
type="button"
|
||||||
|
(click)="saveNewLogin()"
|
||||||
|
[loading]="loading"
|
||||||
|
>
|
||||||
|
{{ "savePasskeyNewLogin" | i18n }}
|
||||||
|
</button>
|
||||||
|
</bit-no-items>
|
||||||
|
</ng-container>
|
||||||
|
</bit-section>
|
||||||
|
|
||||||
|
<!-- Display when the passkey being saved already exists -->
|
||||||
|
<bit-section
|
||||||
|
*ngIf="data.message.type === BrowserFido2MessageTypes.InformExcludedCredentialRequest"
|
||||||
|
>
|
||||||
|
<div class="auth-flow">
|
||||||
|
<p class="subtitle">{{ "passkeyAlreadyExists" | i18n }}</p>
|
||||||
|
<div class="box list">
|
||||||
|
<div class="box-content">
|
||||||
|
<app-fido2-cipher-row
|
||||||
|
*ngFor="let cipherItem of displayedCiphers"
|
||||||
|
[cipher]="cipherItem"
|
||||||
|
title="{{ 'passkeyItem' | i18n }}"
|
||||||
|
(onSelected)="handleCipherItemSelect($event)"
|
||||||
|
></app-fido2-cipher-row>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</div>
|
||||||
<ng-container *ngIf="data.message.type === 'PickCredentialRequest'">
|
|
||||||
<div class="logo">
|
|
||||||
<i class="bwi bwi-shield"></i><span><strong>bit</strong>warden</span>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="data.message.type === 'ConfirmNewCredentialRequest'">
|
</bit-section>
|
||||||
<div class="search">
|
|
||||||
<input
|
<!-- Display when picking a passkey to login with -->
|
||||||
type="{{ searchTypeSearch ? 'search' : 'text' }}"
|
<bit-section *ngIf="data.message.type === BrowserFido2MessageTypes.PickCredentialRequest">
|
||||||
placeholder="{{ 'searchVault' | i18n }}"
|
<!-- Display when matching ciphers exist -->
|
||||||
id="search"
|
<ng-container *ngIf="displayedCiphers.length > 0">
|
||||||
[(ngModel)]="searchText"
|
<ng-container slot="title">{{ "chooseCipherForPasskeyAuth" | i18n }}</ng-container>
|
||||||
(input)="search()"
|
<app-fido2-cipher-row
|
||||||
autocomplete="off"
|
*ngFor="let cipherItem of displayedCiphers"
|
||||||
appAutofocus
|
[cipher]="cipherItem"
|
||||||
/>
|
title="{{ 'passkeyItem' | i18n }}"
|
||||||
<i class="bwi bwi-search" aria-hidden="true"></i>
|
(onSelected)="handleCipherItemSelect($event)"
|
||||||
</div>
|
></app-fido2-cipher-row>
|
||||||
<div class="right">
|
</ng-container>
|
||||||
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
|
|
||||||
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
|
<!-- Display when no matching ciphers exist -->
|
||||||
|
<ng-container *ngIf="!displayedCiphers.length">
|
||||||
|
<bit-no-items class="tw-text-main" [icon]="noResultsIcon">
|
||||||
|
<ng-container slot="title">No matching logins for this site</ng-container>
|
||||||
|
<ng-container slot="description">Search or save passkey as new login</ng-container>
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
slot="button"
|
||||||
|
type="button"
|
||||||
|
(click)="saveNewLogin()"
|
||||||
|
[loading]="loading"
|
||||||
|
>
|
||||||
|
{{ "savePasskeyNewLogin" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</bit-no-items>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</bit-section>
|
||||||
|
|
||||||
<ng-container>
|
<!-- Display when initiating passkey login, but no cooresponding cipher is found in the vault -->
|
||||||
<ng-container
|
<bit-section
|
||||||
*ngIf="
|
*ngIf="data.message.type === BrowserFido2MessageTypes.InformCredentialNotFoundRequest"
|
||||||
data.message.type === 'PickCredentialRequest' ||
|
>
|
||||||
data.message.type === 'ConfirmNewCredentialRequest'
|
<div class="auth-flow">
|
||||||
"
|
<p class="subtitle">{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
|
||||||
>
|
</div>
|
||||||
<div class="auth-flow">
|
<button type="button" class="btn primary block" (click)="abort(false)">
|
||||||
<p class="subtitle" appA11yTitle="{{ subtitleText | i18n }}">
|
<span [hidden]="loading">{{ "close" | i18n }}</span>
|
||||||
{{ subtitleText | i18n }}
|
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
|
||||||
</p>
|
</button>
|
||||||
<!-- Display when ciphers exist -->
|
</bit-section>
|
||||||
<ng-container *ngIf="displayedCiphers.length > 0">
|
|
||||||
<div class="box list">
|
|
||||||
<div class="box-content">
|
|
||||||
<app-fido2-cipher-row
|
|
||||||
*ngFor="let cipherItem of displayedCiphers"
|
|
||||||
[cipher]="cipherItem"
|
|
||||||
[isSearching]="searchPending"
|
|
||||||
title="{{ 'passkeyItem' | i18n }}"
|
|
||||||
(onSelected)="selectedPasskey($event)"
|
|
||||||
[isSelected]="cipher === cipherItem"
|
|
||||||
></app-fido2-cipher-row>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="box">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
(click)="submit()"
|
|
||||||
class="btn primary block"
|
|
||||||
appA11yTitle="{{ credentialText | i18n }}"
|
|
||||||
>
|
|
||||||
<span [hidden]="loading">
|
|
||||||
{{ credentialText | i18n }}
|
|
||||||
</span>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-lg bwi-spin"
|
|
||||||
[hidden]="!loading"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="!displayedCiphers.length">
|
|
||||||
<div class="box">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
(click)="saveNewLogin()"
|
|
||||||
class="btn primary block"
|
|
||||||
appA11yTitle="{{ 'savePasskeyNewLogin' | i18n }}"
|
|
||||||
>
|
|
||||||
<span [hidden]="loading">
|
|
||||||
{{ "savePasskeyNewLogin" | i18n }}
|
|
||||||
</span>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-lg bwi-spin"
|
|
||||||
[hidden]="!loading"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="data.message.type === 'InformExcludedCredentialRequest'">
|
|
||||||
<div class="auth-flow">
|
|
||||||
<p class="subtitle">{{ "passkeyAlreadyExists" | i18n }}</p>
|
|
||||||
<div class="box list">
|
|
||||||
<div class="box-content">
|
|
||||||
<app-fido2-cipher-row
|
|
||||||
*ngFor="let cipherItem of displayedCiphers"
|
|
||||||
[cipher]="cipherItem"
|
|
||||||
title="{{ 'passkeyItem' | i18n }}"
|
|
||||||
(onSelected)="selectedPasskey($event)"
|
|
||||||
[isSelected]="cipher === cipherItem"
|
|
||||||
></app-fido2-cipher-row>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn primary block" (click)="viewPasskey()">
|
|
||||||
<span [hidden]="loading">{{ "viewItem" | i18n }}</span>
|
|
||||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="data.message.type === 'InformCredentialNotFoundRequest'">
|
|
||||||
<div class="auth-flow">
|
|
||||||
<p class="subtitle">{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn primary block" (click)="abort(false)">
|
|
||||||
<span [hidden]="loading">{{ "close" | i18n }}</span>
|
|
||||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<app-fido2-use-browser-link></app-fido2-use-browser-link>
|
<app-fido2-use-browser-link></app-fido2-use-browser-link>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</popup-page>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { FormsModule } from "@angular/forms";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
|
@ -13,13 +15,14 @@ import {
|
||||||
takeUntil,
|
takeUntil,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
import { SecureNoteType, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
@ -27,17 +30,39 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"
|
||||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||||
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import {
|
||||||
|
ButtonModule,
|
||||||
|
DialogService,
|
||||||
|
Icons,
|
||||||
|
ItemModule,
|
||||||
|
NoItemsModule,
|
||||||
|
SearchModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
} from "@bitwarden/components";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service";
|
import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service";
|
||||||
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window";
|
import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window";
|
||||||
import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service";
|
import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service";
|
||||||
import {
|
import {
|
||||||
BrowserFido2Message,
|
BrowserFido2Message,
|
||||||
BrowserFido2UserInterfaceSession,
|
BrowserFido2UserInterfaceSession,
|
||||||
|
BrowserFido2MessageTypes,
|
||||||
} from "../../fido2/services/browser-fido2-user-interface.service";
|
} from "../../fido2/services/browser-fido2-user-interface.service";
|
||||||
|
|
||||||
|
import { Fido2CipherRowComponent } from "./fido2-cipher-row.component";
|
||||||
|
import { Fido2UseBrowserLinkComponent } from "./fido2-use-browser-link.component";
|
||||||
|
|
||||||
|
const PasskeyActions = {
|
||||||
|
Register: "register",
|
||||||
|
Authenticate: "authenticate",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type PasskeyActionValue = (typeof PasskeyActions)[keyof typeof PasskeyActions];
|
||||||
|
|
||||||
interface ViewData {
|
interface ViewData {
|
||||||
message: BrowserFido2Message;
|
message: BrowserFido2Message;
|
||||||
fallbackSupported: boolean;
|
fallbackSupported: boolean;
|
||||||
|
@ -46,28 +71,45 @@ interface ViewData {
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-fido2",
|
selector: "app-fido2",
|
||||||
templateUrl: "fido2.component.html",
|
templateUrl: "fido2.component.html",
|
||||||
styleUrls: [],
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ButtonModule,
|
||||||
|
CommonModule,
|
||||||
|
Fido2CipherRowComponent,
|
||||||
|
Fido2UseBrowserLinkComponent,
|
||||||
|
FormsModule,
|
||||||
|
ItemModule,
|
||||||
|
JslibModule,
|
||||||
|
NoItemsModule,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
PopupPageComponent,
|
||||||
|
SearchModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class Fido2Component implements OnInit, OnDestroy {
|
export class Fido2Component implements OnInit, OnDestroy {
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private hasSearched = false;
|
|
||||||
|
|
||||||
protected cipher: CipherView;
|
|
||||||
protected searchTypeSearch = false;
|
|
||||||
protected searchPending = false;
|
|
||||||
protected searchText: string;
|
|
||||||
protected url: string;
|
|
||||||
protected hostname: string;
|
|
||||||
protected data$: Observable<ViewData>;
|
|
||||||
protected sessionId?: string;
|
|
||||||
protected senderTabId?: string;
|
|
||||||
protected ciphers?: CipherView[] = [];
|
|
||||||
protected displayedCiphers?: CipherView[] = [];
|
|
||||||
protected loading = false;
|
|
||||||
protected subtitleText: string;
|
|
||||||
protected credentialText: string;
|
|
||||||
|
|
||||||
private message$ = new BehaviorSubject<BrowserFido2Message>(null);
|
private message$ = new BehaviorSubject<BrowserFido2Message>(null);
|
||||||
|
private hasSearched = false;
|
||||||
|
protected BrowserFido2MessageTypes = BrowserFido2MessageTypes;
|
||||||
|
protected cipher: CipherView;
|
||||||
|
protected ciphers?: CipherView[] = [];
|
||||||
|
protected data$: Observable<ViewData>;
|
||||||
|
protected displayedCiphers?: CipherView[] = [];
|
||||||
|
protected equivalentDomains: Set<string>;
|
||||||
|
protected equivalentDomainsURL: string;
|
||||||
|
protected hostname: string;
|
||||||
|
protected loading = false;
|
||||||
|
protected noResultsIcon = Icons.NoResults;
|
||||||
|
protected passkeyAction: PasskeyActionValue = PasskeyActions.Register;
|
||||||
|
protected PasskeyActions = PasskeyActions;
|
||||||
|
protected searchText: string;
|
||||||
|
protected searchTypeSearch = false;
|
||||||
|
protected senderTabId?: string;
|
||||||
|
protected sessionId?: string;
|
||||||
|
protected showNewPasskeyButton: boolean = false;
|
||||||
|
protected url: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
@ -80,8 +122,8 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private browserMessagingApi: ZonedMessageListenerService,
|
private browserMessagingApi: ZonedMessageListenerService,
|
||||||
private passwordRepromptService: PasswordRepromptService,
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
private fido2UserVerificationService: Fido2UserVerificationService,
|
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
|
private fido2UserVerificationService: Fido2UserVerificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
@ -107,7 +149,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
this.url = queryParams.senderUrl;
|
this.url = queryParams.senderUrl;
|
||||||
// For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
|
// For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
|
||||||
if (
|
if (
|
||||||
message.type === "NewSessionCreatedRequest" &&
|
message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest &&
|
||||||
message.sessionId !== queryParams.sessionId
|
message.sessionId !== queryParams.sessionId
|
||||||
) {
|
) {
|
||||||
this.abort(false);
|
this.abort(false);
|
||||||
|
@ -119,7 +161,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === "AbortRequest") {
|
if (message.type === BrowserFido2MessageTypes.AbortRequest) {
|
||||||
this.abort(false);
|
this.abort(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -137,7 +179,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
filter((message) => message != undefined),
|
filter((message) => message != undefined),
|
||||||
concatMap(async (message) => {
|
concatMap(async (message) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "ConfirmNewCredentialRequest": {
|
case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: {
|
||||||
const equivalentDomains = await firstValueFrom(
|
const equivalentDomains = await firstValueFrom(
|
||||||
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
||||||
);
|
);
|
||||||
|
@ -145,19 +187,22 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
||||||
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
|
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.displayedCiphers = this.ciphers.filter(
|
this.displayedCiphers = this.ciphers.filter(
|
||||||
(cipher) =>
|
(cipher) =>
|
||||||
cipher.login.matchesUri(this.url, equivalentDomains) &&
|
cipher.login.matchesUri(this.url, equivalentDomains) &&
|
||||||
this.hasNoOtherPasskeys(cipher, message.userHandle),
|
this.cipherHasNoOtherPasskeys(cipher, message.userHandle),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.displayedCiphers.length > 0) {
|
this.passkeyAction = PasskeyActions.Register;
|
||||||
this.selectedPasskey(this.displayedCiphers[0]);
|
|
||||||
}
|
// @TODO fix new cipher creation for other fido2 registration message types and remove `showNewPasskeyButton` from the template
|
||||||
|
this.showNewPasskeyButton = true;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "PickCredentialRequest": {
|
case BrowserFido2MessageTypes.PickCredentialRequest: {
|
||||||
const activeUserId = await firstValueFrom(
|
const activeUserId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
);
|
);
|
||||||
|
@ -170,14 +215,15 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.displayedCiphers = [...this.ciphers];
|
this.displayedCiphers = [...this.ciphers];
|
||||||
if (this.displayedCiphers.length > 0) {
|
|
||||||
this.selectedPasskey(this.displayedCiphers[0]);
|
this.passkeyAction = PasskeyActions.Authenticate;
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "InformExcludedCredentialRequest": {
|
case BrowserFido2MessageTypes.InformExcludedCredentialRequest: {
|
||||||
const activeUserId = await firstValueFrom(
|
const activeUserId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
);
|
);
|
||||||
|
@ -190,40 +236,42 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.displayedCiphers = [...this.ciphers];
|
this.displayedCiphers = [...this.ciphers];
|
||||||
|
|
||||||
if (this.displayedCiphers.length > 0) {
|
this.passkeyAction = PasskeyActions.Register;
|
||||||
this.selectedPasskey(this.displayedCiphers[0]);
|
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case BrowserFido2MessageTypes.InformCredentialNotFoundRequest: {
|
||||||
|
this.passkeyAction = PasskeyActions.Authenticate;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.subtitleText =
|
|
||||||
this.displayedCiphers.length > 0
|
|
||||||
? this.getCredentialSubTitleText(message.type)
|
|
||||||
: "noMatchingPasskeyLogin";
|
|
||||||
|
|
||||||
this.credentialText = this.getCredentialButtonText(message.type);
|
|
||||||
return {
|
return {
|
||||||
message,
|
message,
|
||||||
fallbackSupported: "fallbackSupported" in message && message.fallbackSupported,
|
fallbackSupported: "fallbackSupported" in message && message.fallbackSupported,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
);
|
);
|
||||||
|
|
||||||
queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => {
|
queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => {
|
||||||
this.send({
|
this.send({
|
||||||
sessionId: queryParams.sessionId,
|
sessionId: queryParams.sessionId,
|
||||||
type: "ConnectResponse",
|
type: BrowserFido2MessageTypes.ConnectResponse,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async submit() {
|
protected async submit() {
|
||||||
const data = this.message$.value;
|
const data = this.message$.value;
|
||||||
if (data?.type === "PickCredentialRequest") {
|
|
||||||
|
if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) {
|
||||||
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
|
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
|
||||||
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||||
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
|
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
|
||||||
|
@ -231,10 +279,10 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
this.send({
|
this.send({
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
cipherId: this.cipher.id,
|
cipherId: this.cipher.id,
|
||||||
type: "PickCredentialResponse",
|
type: BrowserFido2MessageTypes.PickCredentialResponse,
|
||||||
userVerified,
|
userVerified,
|
||||||
});
|
});
|
||||||
} else if (data?.type === "ConfirmNewCredentialRequest") {
|
} else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||||
if (this.cipher.login.hasFido2Credentials) {
|
if (this.cipher.login.hasFido2Credentials) {
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "overwritePasskey" },
|
title: { key: "overwritePasskey" },
|
||||||
|
@ -254,7 +302,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
this.send({
|
this.send({
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
cipherId: this.cipher.id,
|
cipherId: this.cipher.id,
|
||||||
type: "ConfirmNewCredentialResponse",
|
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
|
||||||
userVerified,
|
userVerified,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -264,7 +312,8 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
|
|
||||||
protected async saveNewLogin() {
|
protected async saveNewLogin() {
|
||||||
const data = this.message$.value;
|
const data = this.message$.value;
|
||||||
if (data?.type === "ConfirmNewCredentialRequest") {
|
|
||||||
|
if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||||
const name = data.credentialName || data.rpId;
|
const name = data.credentialName || data.rpId;
|
||||||
// TODO: Revert to check for user verification once user verification for passkeys is approved for production.
|
// TODO: Revert to check for user verification once user verification for passkeys is approved for production.
|
||||||
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||||
|
@ -274,7 +323,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
this.send({
|
this.send({
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
cipherId: this.cipher?.id,
|
cipherId: this.cipher?.id,
|
||||||
type: "ConfirmNewCredentialResponse",
|
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
|
||||||
userVerified: data.userVerification,
|
userVerified: data.userVerification,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -282,59 +331,47 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCredentialSubTitleText(messageType: string): string {
|
async handleCipherItemSelect(item: CipherView) {
|
||||||
return messageType == "ConfirmNewCredentialRequest" ? "choosePasskey" : "logInWithPasskey";
|
|
||||||
}
|
|
||||||
|
|
||||||
getCredentialButtonText(messageType: string): string {
|
|
||||||
return messageType == "ConfirmNewCredentialRequest" ? "savePasskey" : "confirm";
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedPasskey(item: CipherView) {
|
|
||||||
this.cipher = item;
|
this.cipher = item;
|
||||||
|
|
||||||
|
await this.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
viewPasskey() {
|
async addCipher() {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/view-cipher"], {
|
|
||||||
queryParams: {
|
|
||||||
cipherId: this.cipher.id,
|
|
||||||
uilocation: "popout",
|
|
||||||
senderTabId: this.senderTabId,
|
|
||||||
sessionId: this.sessionId,
|
|
||||||
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addCipher() {
|
|
||||||
const data = this.message$.value;
|
const data = this.message$.value;
|
||||||
|
|
||||||
if (data?.type !== "ConfirmNewCredentialRequest") {
|
if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||||
return;
|
await this.router.navigate(["/add-cipher"], {
|
||||||
|
queryParams: {
|
||||||
|
type: CipherType.Login.toString(),
|
||||||
|
name: data.credentialName || data.rpId,
|
||||||
|
uri: this.url,
|
||||||
|
uilocation: "popout",
|
||||||
|
username: data.userName,
|
||||||
|
senderTabId: this.senderTabId,
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
userVerification: data.userVerification,
|
||||||
|
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
return;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
}
|
||||||
this.router.navigate(["/add-cipher"], {
|
|
||||||
queryParams: {
|
async getEquivalentDomains() {
|
||||||
name: data.credentialName || data.rpId,
|
if (this.equivalentDomainsURL !== this.url) {
|
||||||
uri: this.url,
|
this.equivalentDomainsURL = this.url;
|
||||||
type: CipherType.Login.toString(),
|
this.equivalentDomains = await firstValueFrom(
|
||||||
uilocation: "popout",
|
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
||||||
username: data.userName,
|
);
|
||||||
senderTabId: this.senderTabId,
|
}
|
||||||
sessionId: this.sessionId,
|
|
||||||
userVerification: data.userVerification,
|
return this.equivalentDomains;
|
||||||
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async search() {
|
protected async search() {
|
||||||
this.hasSearched = await this.searchService.isSearchable(this.searchText);
|
this.hasSearched = await this.searchService.isSearchable(this.searchText);
|
||||||
this.searchPending = true;
|
|
||||||
if (this.hasSearched) {
|
if (this.hasSearched) {
|
||||||
this.displayedCiphers = await this.searchService.searchCiphers(
|
this.displayedCiphers = await this.searchService.searchCiphers(
|
||||||
this.searchText,
|
this.searchText,
|
||||||
|
@ -342,15 +379,11 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
this.ciphers,
|
this.ciphers,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const equivalentDomains = await firstValueFrom(
|
const equivalentDomains = await this.getEquivalentDomains();
|
||||||
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
|
||||||
);
|
|
||||||
this.displayedCiphers = this.ciphers.filter((cipher) =>
|
this.displayedCiphers = this.ciphers.filter((cipher) =>
|
||||||
cipher.login.matchesUri(this.url, equivalentDomains),
|
cipher.login.matchesUri(this.url, equivalentDomains),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.searchPending = false;
|
|
||||||
this.selectedPasskey(this.displayedCiphers[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abort(fallback: boolean) {
|
abort(fallback: boolean) {
|
||||||
|
@ -361,7 +394,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
unload(fallback = false) {
|
unload(fallback = false) {
|
||||||
this.send({
|
this.send({
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
type: "AbortResponse",
|
type: BrowserFido2MessageTypes.AbortResponse,
|
||||||
fallbackRequested: fallback,
|
fallbackRequested: fallback,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -427,13 +460,11 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||||
* This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle
|
* This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle
|
||||||
* @param userHandle
|
* @param userHandle
|
||||||
*/
|
*/
|
||||||
private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean {
|
private cipherHasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean {
|
||||||
if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) {
|
if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cipher.login.fido2Credentials.some((passkey) => {
|
return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle);
|
||||||
passkey.userHandle === userHandle;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-repromp
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||||
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
|
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
|
||||||
|
import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils";
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
|
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
|
||||||
|
@ -1095,7 +1096,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||||
fillFields.expYear.maxLength === 4
|
fillFields.expYear.maxLength === 4
|
||||||
) {
|
) {
|
||||||
if (expYear.length === 2) {
|
if (expYear.length === 2) {
|
||||||
expYear = "20" + expYear;
|
expYear = normalizeExpiryYearFormat(expYear);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
this.fieldAttrsContain(fillFields.expYear, "yy") ||
|
this.fieldAttrsContain(fillFields.expYear, "yy") ||
|
||||||
|
@ -1121,7 +1122,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||||
let partYear: string = null;
|
let partYear: string = null;
|
||||||
if (fullYear.length === 2) {
|
if (fullYear.length === 2) {
|
||||||
partYear = fullYear;
|
partYear = fullYear;
|
||||||
fullYear = "20" + fullYear;
|
fullYear = normalizeExpiryYearFormat(fullYear);
|
||||||
} else if (fullYear.length === 4) {
|
} else if (fullYear.length === 4) {
|
||||||
partYear = fullYear.substr(2, 2);
|
partYear = fullYear.substr(2, 2);
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,29 +122,9 @@ export class InlineMenuFieldQualificationService
|
||||||
...this.identityAddressAutoCompleteValues,
|
...this.identityAddressAutoCompleteValues,
|
||||||
...this.identityCountryAutocompleteValues,
|
...this.identityCountryAutocompleteValues,
|
||||||
...this.identityPhoneNumberAutocompleteValues,
|
...this.identityPhoneNumberAutocompleteValues,
|
||||||
|
this.identityCompanyAutocompleteValue,
|
||||||
this.identityPostalCodeAutocompleteValue,
|
this.identityPostalCodeAutocompleteValue,
|
||||||
]);
|
]);
|
||||||
private identityFieldKeywords = [
|
|
||||||
...new Set([
|
|
||||||
...IdentityAutoFillConstants.TitleFieldNames,
|
|
||||||
...IdentityAutoFillConstants.FullNameFieldNames,
|
|
||||||
...IdentityAutoFillConstants.FirstnameFieldNames,
|
|
||||||
...IdentityAutoFillConstants.MiddlenameFieldNames,
|
|
||||||
...IdentityAutoFillConstants.LastnameFieldNames,
|
|
||||||
...IdentityAutoFillConstants.AddressFieldNames,
|
|
||||||
...IdentityAutoFillConstants.Address1FieldNames,
|
|
||||||
...IdentityAutoFillConstants.Address2FieldNames,
|
|
||||||
...IdentityAutoFillConstants.Address3FieldNames,
|
|
||||||
...IdentityAutoFillConstants.PostalCodeFieldNames,
|
|
||||||
...IdentityAutoFillConstants.CityFieldNames,
|
|
||||||
...IdentityAutoFillConstants.StateFieldNames,
|
|
||||||
...IdentityAutoFillConstants.CountryFieldNames,
|
|
||||||
...IdentityAutoFillConstants.CompanyFieldNames,
|
|
||||||
...IdentityAutoFillConstants.PhoneFieldNames,
|
|
||||||
...IdentityAutoFillConstants.EmailFieldNames,
|
|
||||||
...IdentityAutoFillConstants.UserNameFieldNames,
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
private inlineMenuFieldQualificationFlagSet = false;
|
private inlineMenuFieldQualificationFlagSet = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -288,14 +268,7 @@ export class InlineMenuFieldQualificationService
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues)) {
|
return this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
|
||||||
this.keywordsFoundInFieldData(field, this.identityFieldKeywords, false)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -401,6 +401,8 @@ export default class MainBackground {
|
||||||
const logoutCallback = async (logoutReason: LogoutReason, userId?: UserId) =>
|
const logoutCallback = async (logoutReason: LogoutReason, userId?: UserId) =>
|
||||||
await this.logout(logoutReason, userId);
|
await this.logout(logoutReason, userId);
|
||||||
|
|
||||||
|
const runtimeNativeMessagingBackground = () => this.nativeMessagingBackground;
|
||||||
|
|
||||||
const refreshAccessTokenErrorCallback = () => {
|
const refreshAccessTokenErrorCallback = () => {
|
||||||
// Send toast to popup
|
// Send toast to popup
|
||||||
this.messagingService.send("showToast", {
|
this.messagingService.send("showToast", {
|
||||||
|
@ -616,7 +618,9 @@ export default class MainBackground {
|
||||||
|
|
||||||
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
|
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
|
||||||
|
|
||||||
this.biometricsService = new BackgroundBrowserBiometricsService(this.nativeMessagingBackground);
|
this.biometricsService = new BackgroundBrowserBiometricsService(
|
||||||
|
runtimeNativeMessagingBackground,
|
||||||
|
);
|
||||||
|
|
||||||
this.kdfConfigService = new KdfConfigService(this.stateProvider);
|
this.kdfConfigService = new KdfConfigService(this.stateProvider);
|
||||||
|
|
||||||
|
@ -648,7 +652,7 @@ export default class MainBackground {
|
||||||
this.kdfConfigService,
|
this.kdfConfigService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.appIdService = new AppIdService(this.globalStateProvider);
|
this.appIdService = new AppIdService(this.storageService, this.logService);
|
||||||
|
|
||||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||||
this.organizationService = new OrganizationService(this.stateProvider);
|
this.organizationService = new OrganizationService(this.stateProvider);
|
||||||
|
@ -1263,6 +1267,18 @@ export default class MainBackground {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the user is logged out, switch to the next account
|
||||||
|
const active = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
if (active != null) {
|
||||||
|
const authStatus = await firstValueFrom(
|
||||||
|
this.authService.authStatuses$.pipe(map((statuses) => statuses[active.id])),
|
||||||
|
);
|
||||||
|
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||||
|
const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$);
|
||||||
|
await this.switchAccount(nextUpAccount?.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.initOverlayAndTabsBackground();
|
await this.initOverlayAndTabsBackground();
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"short_name": "__MSG_appName__",
|
"short_name": "__MSG_appName__",
|
||||||
"version": "2024.8.1",
|
"version": "2024.8.2",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Bitwarden Inc.",
|
"author": "Bitwarden Inc.",
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"minimum_chrome_version": "102.0",
|
"minimum_chrome_version": "102.0",
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"short_name": "__MSG_appName__",
|
"short_name": "__MSG_appName__",
|
||||||
"version": "2024.8.1",
|
"version": "2024.8.2",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Bitwarden Inc.",
|
"author": "Bitwarden Inc.",
|
||||||
|
|
|
@ -6,20 +6,20 @@ import { BrowserBiometricsService } from "./browser-biometrics.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BackgroundBrowserBiometricsService extends BrowserBiometricsService {
|
export class BackgroundBrowserBiometricsService extends BrowserBiometricsService {
|
||||||
constructor(private nativeMessagingBackground: NativeMessagingBackground) {
|
constructor(private nativeMessagingBackground: () => NativeMessagingBackground) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticateBiometric(): Promise<boolean> {
|
async authenticateBiometric(): Promise<boolean> {
|
||||||
const responsePromise = this.nativeMessagingBackground.getResponse();
|
const responsePromise = this.nativeMessagingBackground().getResponse();
|
||||||
await this.nativeMessagingBackground.send({ command: "biometricUnlock" });
|
await this.nativeMessagingBackground().send({ command: "biometricUnlock" });
|
||||||
const response = await responsePromise;
|
const response = await responsePromise;
|
||||||
return response.response === "unlocked";
|
return response.response === "unlocked";
|
||||||
}
|
}
|
||||||
|
|
||||||
async isBiometricUnlockAvailable(): Promise<boolean> {
|
async isBiometricUnlockAvailable(): Promise<boolean> {
|
||||||
const responsePromise = this.nativeMessagingBackground.getResponse();
|
const responsePromise = this.nativeMessagingBackground().getResponse();
|
||||||
await this.nativeMessagingBackground.send({ command: "biometricUnlockAvailable" });
|
await this.nativeMessagingBackground().send({ command: "biometricUnlockAvailable" });
|
||||||
const response = await responsePromise;
|
const response = await responsePromise;
|
||||||
return response.response === "available";
|
return response.response === "available";
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,6 +199,9 @@ export const routerTransition = trigger("routerTransition", [
|
||||||
transition("vault-settings => sync", inSlideLeft),
|
transition("vault-settings => sync", inSlideLeft),
|
||||||
transition("sync => vault-settings", outSlideRight),
|
transition("sync => vault-settings", outSlideRight),
|
||||||
|
|
||||||
|
transition("vault-settings => trash", inSlideLeft),
|
||||||
|
transition("trash => vault-settings", outSlideRight),
|
||||||
|
|
||||||
// Appearance settings
|
// Appearance settings
|
||||||
transition("tabs => appearance", inSlideLeft),
|
transition("tabs => appearance", inSlideLeft),
|
||||||
transition("appearance => tabs", outSlideRight),
|
transition("appearance => tabs", outSlideRight),
|
||||||
|
|
|
@ -41,6 +41,7 @@ import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component"
|
||||||
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
||||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||||
|
import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component";
|
||||||
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
||||||
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
||||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||||
|
@ -58,6 +59,7 @@ import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/pass
|
||||||
import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component";
|
import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component";
|
||||||
import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component";
|
import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component";
|
||||||
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
|
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
|
||||||
|
import { SendAddEditComponent as SendAddEditV2Component } from "../tools/popup/send-v2/add-edit/send-add-edit.component";
|
||||||
import { SendCreatedComponent } from "../tools/popup/send-v2/send-created/send-created.component";
|
import { SendCreatedComponent } from "../tools/popup/send-v2/send-created/send-created.component";
|
||||||
import { SendV2Component } from "../tools/popup/send-v2/send-v2.component";
|
import { SendV2Component } from "../tools/popup/send-v2/send-v2.component";
|
||||||
import { AboutPageV2Component } from "../tools/popup/settings/about-page/about-page-v2.component";
|
import { AboutPageV2Component } from "../tools/popup/settings/about-page/about-page-v2.component";
|
||||||
|
@ -91,6 +93,7 @@ import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.
|
||||||
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
|
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
|
||||||
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
||||||
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
||||||
|
import { TrashComponent } from "../vault/popup/settings/trash.component";
|
||||||
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
|
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
|
||||||
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
|
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
|
||||||
|
|
||||||
|
@ -126,12 +129,11 @@ const routes: Routes = [
|
||||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||||
data: { state: "home" },
|
data: { state: "home" },
|
||||||
},
|
},
|
||||||
{
|
...extensionRefreshSwap(Fido2V1Component, Fido2Component, {
|
||||||
path: "fido2",
|
path: "fido2",
|
||||||
component: Fido2Component,
|
|
||||||
canActivate: [fido2AuthGuard],
|
canActivate: [fido2AuthGuard],
|
||||||
data: { state: "fido2" },
|
data: { state: "fido2" },
|
||||||
},
|
}),
|
||||||
{
|
{
|
||||||
path: "login",
|
path: "login",
|
||||||
component: LoginComponent,
|
component: LoginComponent,
|
||||||
|
@ -303,7 +305,6 @@ const routes: Routes = [
|
||||||
},
|
},
|
||||||
...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, {
|
...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, {
|
||||||
path: "notifications",
|
path: "notifications",
|
||||||
component: NotificationsSettingsV1Component,
|
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { state: "notifications" },
|
data: { state: "notifications" },
|
||||||
}),
|
}),
|
||||||
|
@ -337,7 +338,6 @@ const routes: Routes = [
|
||||||
},
|
},
|
||||||
...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, {
|
...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, {
|
||||||
path: "excluded-domains",
|
path: "excluded-domains",
|
||||||
component: ExcludedDomainsV1Component,
|
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { state: "excluded-domains" },
|
data: { state: "excluded-domains" },
|
||||||
}),
|
}),
|
||||||
|
@ -363,18 +363,16 @@ const routes: Routes = [
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { state: "send-type" },
|
data: { state: "send-type" },
|
||||||
},
|
},
|
||||||
{
|
...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, {
|
||||||
path: "add-send",
|
path: "add-send",
|
||||||
component: SendAddEditComponent,
|
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { state: "add-send" },
|
data: { state: "add-send" },
|
||||||
},
|
}),
|
||||||
{
|
...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, {
|
||||||
path: "edit-send",
|
path: "edit-send",
|
||||||
component: SendAddEditComponent,
|
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { state: "edit-send" },
|
data: { state: "edit-send" },
|
||||||
},
|
}),
|
||||||
{
|
{
|
||||||
path: "send-created",
|
path: "send-created",
|
||||||
component: SendCreatedComponent,
|
component: SendCreatedComponent,
|
||||||
|
@ -496,6 +494,12 @@ const routes: Routes = [
|
||||||
component: AccountSwitcherComponent,
|
component: AccountSwitcherComponent,
|
||||||
data: { state: "account-switcher", doNotSaveUrl: true },
|
data: { state: "account-switcher", doNotSaveUrl: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "trash",
|
||||||
|
component: TrashComponent,
|
||||||
|
canActivate: [authGuard],
|
||||||
|
data: { state: "trash" },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
|
@ -35,8 +35,11 @@ import { SsoComponent } from "../auth/popup/sso.component";
|
||||||
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
||||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||||
|
import { Fido2CipherRowV1Component } from "../autofill/popup/fido2/fido2-cipher-row-v1.component";
|
||||||
import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component";
|
import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component";
|
||||||
|
import { Fido2UseBrowserLinkV1Component } from "../autofill/popup/fido2/fido2-use-browser-link-v1.component";
|
||||||
import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component";
|
import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component";
|
||||||
|
import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component";
|
||||||
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
||||||
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
||||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||||
|
@ -112,6 +115,9 @@ import "../platform/popup/locales";
|
||||||
ServicesModule,
|
ServicesModule,
|
||||||
DialogModule,
|
DialogModule,
|
||||||
ExcludedDomainsComponent,
|
ExcludedDomainsComponent,
|
||||||
|
Fido2CipherRowComponent,
|
||||||
|
Fido2Component,
|
||||||
|
Fido2UseBrowserLinkComponent,
|
||||||
FilePopoutCalloutComponent,
|
FilePopoutCalloutComponent,
|
||||||
AvatarModule,
|
AvatarModule,
|
||||||
AccountComponent,
|
AccountComponent,
|
||||||
|
@ -140,8 +146,8 @@ import "../platform/popup/locales";
|
||||||
CurrentTabComponent,
|
CurrentTabComponent,
|
||||||
EnvironmentComponent,
|
EnvironmentComponent,
|
||||||
ExcludedDomainsV1Component,
|
ExcludedDomainsV1Component,
|
||||||
Fido2CipherRowComponent,
|
Fido2CipherRowV1Component,
|
||||||
Fido2UseBrowserLinkComponent,
|
Fido2UseBrowserLinkV1Component,
|
||||||
FolderAddEditComponent,
|
FolderAddEditComponent,
|
||||||
FoldersComponent,
|
FoldersComponent,
|
||||||
VaultFilterComponent,
|
VaultFilterComponent,
|
||||||
|
@ -180,7 +186,7 @@ import "../platform/popup/locales";
|
||||||
ViewCustomFieldsComponent,
|
ViewCustomFieldsComponent,
|
||||||
RemovePasswordComponent,
|
RemovePasswordComponent,
|
||||||
VaultSelectComponent,
|
VaultSelectComponent,
|
||||||
Fido2Component,
|
Fido2V1Component,
|
||||||
AutofillV1Component,
|
AutofillV1Component,
|
||||||
EnvironmentSelectorComponent,
|
EnvironmentSelectorComponent,
|
||||||
],
|
],
|
||||||
|
|
|
@ -217,7 +217,7 @@ app-vault-attachments {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app-fido2 {
|
app-fido2-v1 {
|
||||||
.auth-wrapper {
|
.auth-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header" [pageTitle]="headerText" showBackButton></popup-header>
|
||||||
|
|
||||||
|
<tools-send-form
|
||||||
|
formId="sendForm"
|
||||||
|
[config]="config"
|
||||||
|
(sendSaved)="onSendSaved()"
|
||||||
|
[submitBtn]="submitBtn"
|
||||||
|
>
|
||||||
|
</tools-send-form>
|
||||||
|
|
||||||
|
<popup-footer slot="footer">
|
||||||
|
<button bitButton type="submit" form="sendForm" buttonType="primary" #submitBtn>
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
</popup-footer>
|
||||||
|
</popup-page>
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { CommonModule, Location } from "@angular/common";
|
||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
|
import { FormsModule } from "@angular/forms";
|
||||||
|
import { ActivatedRoute, Params } from "@angular/router";
|
||||||
|
import { map, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||||
|
import { SendId } from "@bitwarden/common/types/guid";
|
||||||
|
import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components";
|
||||||
|
import {
|
||||||
|
DefaultSendFormConfigService,
|
||||||
|
SendFormConfig,
|
||||||
|
SendFormConfigService,
|
||||||
|
SendFormMode,
|
||||||
|
} from "@bitwarden/send-ui";
|
||||||
|
|
||||||
|
import { SendFormModule } from "../../../../../../../libs/tools/send/send-ui/src/send-form/send-form.module";
|
||||||
|
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
|
||||||
|
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
|
||||||
|
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to parse query parameters for the AddEdit route.
|
||||||
|
*/
|
||||||
|
class QueryParams {
|
||||||
|
constructor(params: Params) {
|
||||||
|
this.sendId = params.sendId;
|
||||||
|
this.type = parseInt(params.type, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the send to edit, empty when it's a new Send
|
||||||
|
*/
|
||||||
|
sendId?: SendId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of send to create.
|
||||||
|
*/
|
||||||
|
type: SendType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for adding or editing a send item.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: "tools-send-add-edit",
|
||||||
|
templateUrl: "send-add-edit.component.html",
|
||||||
|
standalone: true,
|
||||||
|
providers: [{ provide: SendFormConfigService, useClass: DefaultSendFormConfigService }],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SearchModule,
|
||||||
|
JslibModule,
|
||||||
|
FormsModule,
|
||||||
|
ButtonModule,
|
||||||
|
PopupPageComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
PopupFooterComponent,
|
||||||
|
SendFormModule,
|
||||||
|
AsyncActionsModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class SendAddEditComponent {
|
||||||
|
/**
|
||||||
|
* The header text for the component.
|
||||||
|
*/
|
||||||
|
headerText: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration for the send form.
|
||||||
|
*/
|
||||||
|
config: SendFormConfig;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private location: Location,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private addEditFormConfigService: SendFormConfigService,
|
||||||
|
) {
|
||||||
|
this.subscribeToParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the event when the send is saved.
|
||||||
|
*/
|
||||||
|
onSendSaved() {
|
||||||
|
this.location.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to the route query parameters and builds the configuration based on the parameters.
|
||||||
|
*/
|
||||||
|
subscribeToParams(): void {
|
||||||
|
this.route.queryParams
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(),
|
||||||
|
map((params) => new QueryParams(params)),
|
||||||
|
switchMap(async (params) => {
|
||||||
|
let mode: SendFormMode;
|
||||||
|
if (params.sendId == null) {
|
||||||
|
mode = "add";
|
||||||
|
} else {
|
||||||
|
mode = "edit";
|
||||||
|
}
|
||||||
|
const config = await this.addEditFormConfigService.buildConfig(
|
||||||
|
mode,
|
||||||
|
params.sendId,
|
||||||
|
params.type,
|
||||||
|
);
|
||||||
|
return config;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe((config) => {
|
||||||
|
this.config = config;
|
||||||
|
this.headerText = this.getHeaderText(config.mode, config.sendType);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the header text based on the mode and type.
|
||||||
|
* @param mode The mode of the send form.
|
||||||
|
* @param type The type of the send form.
|
||||||
|
* @returns The header text.
|
||||||
|
*/
|
||||||
|
private getHeaderText(mode: SendFormMode, type: SendType) {
|
||||||
|
const headerKey =
|
||||||
|
mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader";
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case SendType.Text:
|
||||||
|
return this.i18nService.t(headerKey, this.i18nService.t("sendTypeText"));
|
||||||
|
case SendType.File:
|
||||||
|
return this.i18nService.t(headerKey, this.i18nService.t("sendTypeFile"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -312,13 +312,13 @@ export class AddEditV2Component implements OnInit {
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case CipherType.Login:
|
case CipherType.Login:
|
||||||
return this.i18nService.t(partOne, this.i18nService.t("typeLogin"));
|
return this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLocaleLowerCase());
|
||||||
case CipherType.Card:
|
case CipherType.Card:
|
||||||
return this.i18nService.t(partOne, this.i18nService.t("typeCard"));
|
return this.i18nService.t(partOne, this.i18nService.t("typeCard").toLocaleLowerCase());
|
||||||
case CipherType.Identity:
|
case CipherType.Identity:
|
||||||
return this.i18nService.t(partOne, this.i18nService.t("typeIdentity"));
|
return this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLocaleLowerCase());
|
||||||
case CipherType.SecureNote:
|
case CipherType.SecureNote:
|
||||||
return this.i18nService.t(partOne, this.i18nService.t("note"));
|
return this.i18nService.t(partOne, this.i18nService.t("note").toLocaleLowerCase());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,13 @@
|
||||||
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher">
|
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher">
|
||||||
{{ "copyUsername" | i18n }}
|
{{ "copyUsername" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" bitMenuItem appCopyField="password" [cipher]="cipher">
|
<button
|
||||||
|
*ngIf="cipher.viewPassword"
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
appCopyField="password"
|
||||||
|
[cipher]="cipher"
|
||||||
|
>
|
||||||
{{ "copyPassword" | i18n }}
|
{{ "copyPassword" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" bitMenuItem appCopyField="totp" [cipher]="cipher">
|
<button type="button" bitMenuItem appCopyField="totp" [cipher]="cipher">
|
||||||
|
|
|
@ -5,13 +5,30 @@
|
||||||
|
|
||||||
<app-cipher-view *ngIf="cipher" [cipher]="cipher"></app-cipher-view>
|
<app-cipher-view *ngIf="cipher" [cipher]="cipher"></app-cipher-view>
|
||||||
|
|
||||||
<popup-footer slot="footer">
|
<popup-footer slot="footer" *ngIf="showFooter()">
|
||||||
<button buttonType="primary" type="button" bitButton (click)="editCipher()">
|
<button
|
||||||
|
*ngIf="!cipher.isDeleted"
|
||||||
|
buttonType="primary"
|
||||||
|
type="button"
|
||||||
|
bitButton
|
||||||
|
(click)="editCipher()"
|
||||||
|
>
|
||||||
{{ "edit" | i18n }}
|
{{ "edit" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="cipher.isDeleted && cipher.edit"
|
||||||
|
buttonType="primary"
|
||||||
|
type="button"
|
||||||
|
bitButton
|
||||||
|
[bitAction]="restore"
|
||||||
|
>
|
||||||
|
{{ "restore" | i18n }}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
slot="end"
|
slot="end"
|
||||||
*ngIf="cipher && cipher.edit"
|
*ngIf="cipher.edit"
|
||||||
[bitAction]="delete"
|
[bitAction]="delete"
|
||||||
type="button"
|
type="button"
|
||||||
buttonType="danger"
|
buttonType="danger"
|
||||||
|
|
|
@ -162,9 +162,28 @@ export class ViewV2Component {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
restore = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await this.cipherService.restoreWithServer(this.cipher.id);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.router.navigate(["/vault"]);
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("restoredItem"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
protected deleteCipher() {
|
protected deleteCipher() {
|
||||||
return this.cipher.isDeleted
|
return this.cipher.isDeleted
|
||||||
? this.cipherService.deleteWithServer(this.cipher.id)
|
? this.cipherService.deleteWithServer(this.cipher.id)
|
||||||
: this.cipherService.softDeleteWithServer(this.cipher.id);
|
: this.cipherService.softDeleteWithServer(this.cipher.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected showFooter(): boolean {
|
||||||
|
return this.cipher && (!this.cipher.isDeleted || (this.cipher.isDeleted && this.cipher.edit));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||||
|
import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
@ -182,6 +183,11 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||||
const { isFido2Session, sessionId, userVerification } = fido2SessionData;
|
const { isFido2Session, sessionId, userVerification } = fido2SessionData;
|
||||||
const inFido2PopoutWindow = BrowserPopupUtils.inPopout(window) && isFido2Session;
|
const inFido2PopoutWindow = BrowserPopupUtils.inPopout(window) && isFido2Session;
|
||||||
|
|
||||||
|
// normalize card expiry year on save
|
||||||
|
if (this.cipher.type === this.cipherType.Card) {
|
||||||
|
this.cipher.card.expYear = normalizeExpiryYearFormat(this.cipher.card.expYear);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
|
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
|
||||||
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -358,6 +358,24 @@ describe("VaultPopupItemsService", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("deletedCiphers$", () => {
|
||||||
|
it("should return deleted ciphers", (done) => {
|
||||||
|
const ciphers = [
|
||||||
|
{ id: "1", type: CipherType.Login, name: "Login 1", isDeleted: true },
|
||||||
|
{ id: "2", type: CipherType.Login, name: "Login 2", isDeleted: true },
|
||||||
|
{ id: "3", type: CipherType.Login, name: "Login 3", isDeleted: true },
|
||||||
|
{ id: "4", type: CipherType.Login, name: "Login 4", isDeleted: false },
|
||||||
|
] as CipherView[];
|
||||||
|
|
||||||
|
cipherServiceMock.getAllDecrypted.mockResolvedValue(ciphers);
|
||||||
|
|
||||||
|
service.deletedCiphers$.subscribe((deletedCiphers) => {
|
||||||
|
expect(deletedCiphers.length).toBe(3);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("hasFilterApplied$", () => {
|
describe("hasFilterApplied$", () => {
|
||||||
it("should return true if the search term provided is searchable", (done) => {
|
it("should return true if the search term provided is searchable", (done) => {
|
||||||
searchService.isSearchable.mockImplementation(async () => true);
|
searchService.isSearchable.mockImplementation(async () => true);
|
||||||
|
|
|
@ -76,7 +76,7 @@ export class VaultPopupItemsService {
|
||||||
* Observable that contains the list of all decrypted ciphers.
|
* Observable that contains the list of all decrypted ciphers.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private _cipherList$: Observable<PopupCipherView[]> = merge(
|
private _allDecryptedCiphers$: Observable<CipherView[]> = merge(
|
||||||
this.cipherService.ciphers$,
|
this.cipherService.ciphers$,
|
||||||
this.cipherService.localData$,
|
this.cipherService.localData$,
|
||||||
).pipe(
|
).pipe(
|
||||||
|
@ -84,6 +84,10 @@ export class VaultPopupItemsService {
|
||||||
tap(() => this._ciphersLoading$.next()),
|
tap(() => this._ciphersLoading$.next()),
|
||||||
waitUntilSync(this.syncService),
|
waitUntilSync(this.syncService),
|
||||||
switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())),
|
switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())),
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
private _activeCipherList$: Observable<PopupCipherView[]> = this._allDecryptedCiphers$.pipe(
|
||||||
switchMap((ciphers) =>
|
switchMap((ciphers) =>
|
||||||
combineLatest([
|
combineLatest([
|
||||||
this.organizationService.organizations$,
|
this.organizationService.organizations$,
|
||||||
|
@ -105,11 +109,10 @@ export class VaultPopupItemsService {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
private _filteredCipherList$: Observable<PopupCipherView[]> = combineLatest([
|
private _filteredCipherList$: Observable<PopupCipherView[]> = combineLatest([
|
||||||
this._cipherList$,
|
this._activeCipherList$,
|
||||||
this._searchText$,
|
this._searchText$,
|
||||||
this.vaultPopupListFiltersService.filterFunction$,
|
this.vaultPopupListFiltersService.filterFunction$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
|
@ -208,7 +211,9 @@ export class VaultPopupItemsService {
|
||||||
/**
|
/**
|
||||||
* Observable that indicates whether the user's vault is empty.
|
* Observable that indicates whether the user's vault is empty.
|
||||||
*/
|
*/
|
||||||
emptyVault$: Observable<boolean> = this._cipherList$.pipe(map((ciphers) => !ciphers.length));
|
emptyVault$: Observable<boolean> = this._activeCipherList$.pipe(
|
||||||
|
map((ciphers) => !ciphers.length),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable that indicates whether there are no ciphers to show with the current filter.
|
* Observable that indicates whether there are no ciphers to show with the current filter.
|
||||||
|
@ -232,6 +237,14 @@ export class VaultPopupItemsService {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that contains the list of ciphers that have been deleted.
|
||||||
|
*/
|
||||||
|
deletedCiphers$: Observable<CipherView[]> = this._allDecryptedCiphers$.pipe(
|
||||||
|
map((ciphers) => ciphers.filter((c) => c.isDeleted)),
|
||||||
|
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private vaultSettingsService: VaultSettingsService,
|
private vaultSettingsService: VaultSettingsService,
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<bit-section *ngIf="ciphers?.length">
|
||||||
|
<bit-section-header>
|
||||||
|
<h2 bitTypography="h6">
|
||||||
|
{{ headerText }}
|
||||||
|
</h2>
|
||||||
|
<span bitTypography="body1" slot="end">{{ ciphers.length }}</span>
|
||||||
|
</bit-section-header>
|
||||||
|
<bit-item-group>
|
||||||
|
<bit-item *ngFor="let cipher of ciphers">
|
||||||
|
<a
|
||||||
|
bit-item-content
|
||||||
|
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
||||||
|
(click)="onViewCipher(cipher)"
|
||||||
|
>
|
||||||
|
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||||
|
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||||
|
</a>
|
||||||
|
<ng-container slot="end" *ngIf="cipher.edit">
|
||||||
|
<bit-item-action>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-ellipsis-v"
|
||||||
|
size="small"
|
||||||
|
[attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name"
|
||||||
|
[title]="'moreOptionsTitle' | i18n: cipher.name"
|
||||||
|
[bitMenuTriggerFor]="moreOptions"
|
||||||
|
></button>
|
||||||
|
<bit-menu #moreOptions>
|
||||||
|
<button type="button" bitMenuItem (click)="restore(cipher)">
|
||||||
|
{{ "restore" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitMenuItem (click)="delete(cipher)">
|
||||||
|
{{ "deleteForever" | i18n }}
|
||||||
|
</button>
|
||||||
|
</bit-menu>
|
||||||
|
</bit-item-action>
|
||||||
|
</ng-container>
|
||||||
|
</bit-item>
|
||||||
|
</bit-item-group>
|
||||||
|
</bit-section>
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, Input } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import {
|
||||||
|
DialogService,
|
||||||
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
MenuModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
ToastService,
|
||||||
|
TypographyModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-trash-list-items-container",
|
||||||
|
templateUrl: "trash-list-items-container.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
ItemModule,
|
||||||
|
JslibModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
MenuModule,
|
||||||
|
IconButtonModule,
|
||||||
|
TypographyModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class TrashListItemsContainerComponent {
|
||||||
|
/**
|
||||||
|
* The list of trashed items to display.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
ciphers: CipherView[] = [];
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
headerText: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cipherService: CipherService,
|
||||||
|
private logService: LogService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
|
private router: Router,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async restore(cipher: CipherView) {
|
||||||
|
try {
|
||||||
|
await this.cipherService.restoreWithServer(cipher.id);
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("restoredItem"),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(cipher: CipherView) {
|
||||||
|
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
|
||||||
|
|
||||||
|
if (!repromptPassed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "deleteItem" },
|
||||||
|
content: { key: "permanentlyDeleteItemConfirmation" },
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.cipherService.deleteWithServer(cipher.id);
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("deletedItem"),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onViewCipher(cipher: CipherView) {
|
||||||
|
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
|
||||||
|
if (!repromptPassed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.router.navigate(["/view-cipher"], {
|
||||||
|
queryParams: { cipherId: cipher.id, type: cipher.type },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header" [pageTitle]="'trash' | i18n" showBackButton>
|
||||||
|
<ng-container slot="end">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
</ng-container>
|
||||||
|
</popup-header>
|
||||||
|
<ng-container *ngIf="deletedCiphers$ | async as deletedItems">
|
||||||
|
<bit-callout
|
||||||
|
*ngIf="deletedItems.length"
|
||||||
|
type="warning"
|
||||||
|
title="{{ 'warning' | i18n | titlecase }}"
|
||||||
|
>
|
||||||
|
{{ "trashWarning" | i18n }}
|
||||||
|
</bit-callout>
|
||||||
|
|
||||||
|
<ng-container *ngIf="deletedItems.length; else noDeletedItems">
|
||||||
|
<app-trash-list-items-container
|
||||||
|
[headerText]="'itemsInTrash' | i18n"
|
||||||
|
[ciphers]="deletedItems"
|
||||||
|
></app-trash-list-items-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #noDeletedItems>
|
||||||
|
<bit-no-items
|
||||||
|
[icon]="emptyTrashIcon"
|
||||||
|
class="tw-flex tw-h-full tw-items-center tw-justify-center"
|
||||||
|
>
|
||||||
|
<ng-container slot="title">
|
||||||
|
{{ "noItemsInTrash" | i18n }}
|
||||||
|
</ng-container>
|
||||||
|
<ng-container slot="description">
|
||||||
|
{{ "noItemsInTrashDesc" | i18n }}
|
||||||
|
</ng-container>
|
||||||
|
</bit-no-items>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
</popup-page>
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { CalloutModule, NoItemsModule } from "@bitwarden/components";
|
||||||
|
import { VaultIcons } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
|
import { VaultListItemsContainerComponent } from "../components/vault-v2/vault-list-items-container/vault-list-items-container.component";
|
||||||
|
import { VaultPopupItemsService } from "../services/vault-popup-items.service";
|
||||||
|
|
||||||
|
import { TrashListItemsContainerComponent } from "./trash-list-items-container/trash-list-items-container.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "trash.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
JslibModule,
|
||||||
|
PopupPageComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
PopOutComponent,
|
||||||
|
VaultListItemsContainerComponent,
|
||||||
|
TrashListItemsContainerComponent,
|
||||||
|
CalloutModule,
|
||||||
|
NoItemsModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class TrashComponent {
|
||||||
|
protected deletedCiphers$ = this.vaultPopupItemsService.deletedCiphers$;
|
||||||
|
|
||||||
|
protected emptyTrashIcon = VaultIcons.EmptyTrash;
|
||||||
|
|
||||||
|
constructor(private vaultPopupItemsService: VaultPopupItemsService) {}
|
||||||
|
}
|
|
@ -24,6 +24,12 @@
|
||||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</bit-item>
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content routerLink="/trash">
|
||||||
|
{{ "trash" | i18n }}
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
<bit-item>
|
<bit-item>
|
||||||
<button type="button" bit-item-content (click)="sync()">
|
<button type="button" bit-item-content (click)="sync()">
|
||||||
{{ "syncVaultNow" | i18n }}
|
{{ "syncVaultNow" | i18n }}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"lib": ["ES2021.String"],
|
"lib": ["ES2021.String"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@bitwarden/admin-console": ["../../libs/admin-console/src"],
|
"@bitwarden/admin-console/common": ["../../libs/admin-console/src/common"],
|
||||||
"@bitwarden/angular/*": ["../../libs/angular/src/*"],
|
"@bitwarden/angular/*": ["../../libs/angular/src/*"],
|
||||||
"@bitwarden/auth/common": ["../../libs/auth/src/common"],
|
"@bitwarden/auth/common": ["../../libs/auth/src/common"],
|
||||||
"@bitwarden/auth/angular": ["../../libs/auth/src/angular"],
|
"@bitwarden/auth/angular": ["../../libs/auth/src/angular"],
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"flags": {
|
"flags": {
|
||||||
"enableCipherKeyEncryption": true
|
"enableCipherKeyEncryption": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"flags": {
|
"flags": {
|
||||||
"enableCipherKeyEncryption": true
|
"enableCipherKeyEncryption": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import {
|
||||||
|
OrganizationUserApiService,
|
||||||
|
OrganizationUserConfirmRequest,
|
||||||
|
} from "@bitwarden/admin-console/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
|
||||||
import { OrganizationUserConfirmRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
|
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
|
@ -10,7 +12,7 @@ export class ConfirmCommand {
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private organizationUserService: OrganizationUserService,
|
private organizationUserApiService: OrganizationUserApiService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
|
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
|
||||||
|
@ -42,7 +44,7 @@ export class ConfirmCommand {
|
||||||
if (orgKey == null) {
|
if (orgKey == null) {
|
||||||
throw new Error("No encryption key for this organization.");
|
throw new Error("No encryption key for this organization.");
|
||||||
}
|
}
|
||||||
const orgUser = await this.organizationUserService.getOrganizationUser(
|
const orgUser = await this.organizationUserApiService.getOrganizationUser(
|
||||||
options.organizationId,
|
options.organizationId,
|
||||||
id,
|
id,
|
||||||
);
|
);
|
||||||
|
@ -54,7 +56,7 @@ export class ConfirmCommand {
|
||||||
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey);
|
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey);
|
||||||
const req = new OrganizationUserConfirmRequest();
|
const req = new OrganizationUserConfirmRequest();
|
||||||
req.key = key.encryptedString;
|
req.key = key.encryptedString;
|
||||||
await this.organizationUserService.postOrganizationUserConfirm(
|
await this.organizationUserApiService.postOrganizationUserConfirm(
|
||||||
options.organizationId,
|
options.organizationId,
|
||||||
id,
|
id,
|
||||||
req,
|
req,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
import { ListResponse as ApiListResponse } from "@bitwarden/common/models/response/list.response";
|
import { ListResponse as ApiListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
@ -35,7 +35,7 @@ export class ListCommand {
|
||||||
private collectionService: CollectionService,
|
private collectionService: CollectionService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private organizationUserService: OrganizationUserService,
|
private organizationUserApiService: OrganizationUserApiService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private eventCollectionService: EventCollectionService,
|
private eventCollectionService: EventCollectionService,
|
||||||
) {}
|
) {}
|
||||||
|
@ -211,7 +211,7 @@ export class ListCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.organizationUserService.getAllUsers(options.organizationId);
|
const response = await this.organizationUserApiService.getAllUsers(options.organizationId);
|
||||||
const res = new ListResponse(
|
const res = new ListResponse(
|
||||||
response.data.map((r) => {
|
response.data.map((r) => {
|
||||||
const u = new OrganizationUserResponse();
|
const u = new OrganizationUserResponse();
|
||||||
|
|
|
@ -71,7 +71,7 @@ export class OssServeConfigurator {
|
||||||
this.serviceContainer.collectionService,
|
this.serviceContainer.collectionService,
|
||||||
this.serviceContainer.organizationService,
|
this.serviceContainer.organizationService,
|
||||||
this.serviceContainer.searchService,
|
this.serviceContainer.searchService,
|
||||||
this.serviceContainer.organizationUserService,
|
this.serviceContainer.organizationUserApiService,
|
||||||
this.serviceContainer.apiService,
|
this.serviceContainer.apiService,
|
||||||
this.serviceContainer.eventCollectionService,
|
this.serviceContainer.eventCollectionService,
|
||||||
);
|
);
|
||||||
|
@ -114,7 +114,7 @@ export class OssServeConfigurator {
|
||||||
this.confirmCommand = new ConfirmCommand(
|
this.confirmCommand = new ConfirmCommand(
|
||||||
this.serviceContainer.apiService,
|
this.serviceContainer.apiService,
|
||||||
this.serviceContainer.cryptoService,
|
this.serviceContainer.cryptoService,
|
||||||
this.serviceContainer.organizationUserService,
|
this.serviceContainer.organizationUserApiService,
|
||||||
);
|
);
|
||||||
this.restoreCommand = new RestoreCommand(this.serviceContainer.cipherService);
|
this.restoreCommand = new RestoreCommand(this.serviceContainer.cipherService);
|
||||||
this.shareCommand = new ShareCommand(
|
this.shareCommand = new ShareCommand(
|
||||||
|
|
|
@ -4,6 +4,10 @@ import * as path from "path";
|
||||||
import * as jsdom from "jsdom";
|
import * as jsdom from "jsdom";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
OrganizationUserApiService,
|
||||||
|
DefaultOrganizationUserApiService,
|
||||||
|
} from "@bitwarden/admin-console/common";
|
||||||
import {
|
import {
|
||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
AuthRequestService,
|
AuthRequestService,
|
||||||
|
@ -16,12 +20,10 @@ import {
|
||||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||||
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
|
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
|
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
|
||||||
import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation";
|
|
||||||
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||||
import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service";
|
import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service";
|
||||||
|
@ -184,7 +186,7 @@ export class ServiceContainer {
|
||||||
environmentService: EnvironmentService;
|
environmentService: EnvironmentService;
|
||||||
cipherService: CipherService;
|
cipherService: CipherService;
|
||||||
folderService: InternalFolderService;
|
folderService: InternalFolderService;
|
||||||
organizationUserService: OrganizationUserService;
|
organizationUserApiService: OrganizationUserApiService;
|
||||||
collectionService: CollectionService;
|
collectionService: CollectionService;
|
||||||
vaultTimeoutService: VaultTimeoutService;
|
vaultTimeoutService: VaultTimeoutService;
|
||||||
masterPasswordService: InternalMasterPasswordServiceAbstraction;
|
masterPasswordService: InternalMasterPasswordServiceAbstraction;
|
||||||
|
@ -411,7 +413,7 @@ export class ServiceContainer {
|
||||||
this.kdfConfigService,
|
this.kdfConfigService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.appIdService = new AppIdService(this.globalStateProvider);
|
this.appIdService = new AppIdService(this.storageService, this.logService);
|
||||||
|
|
||||||
const customUserAgent =
|
const customUserAgent =
|
||||||
"Bitwarden_CLI/" +
|
"Bitwarden_CLI/" +
|
||||||
|
@ -492,7 +494,7 @@ export class ServiceContainer {
|
||||||
|
|
||||||
this.providerService = new ProviderService(this.stateProvider);
|
this.providerService = new ProviderService(this.stateProvider);
|
||||||
|
|
||||||
this.organizationUserService = new OrganizationUserServiceImplementation(this.apiService);
|
this.organizationUserApiService = new DefaultOrganizationUserApiService(this.apiService);
|
||||||
|
|
||||||
this.policyApiService = new PolicyApiService(this.policyService, this.apiService);
|
this.policyApiService = new PolicyApiService(this.policyService, this.apiService);
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,7 @@ export class VaultProgram extends BaseProgram {
|
||||||
this.serviceContainer.collectionService,
|
this.serviceContainer.collectionService,
|
||||||
this.serviceContainer.organizationService,
|
this.serviceContainer.organizationService,
|
||||||
this.serviceContainer.searchService,
|
this.serviceContainer.searchService,
|
||||||
this.serviceContainer.organizationUserService,
|
this.serviceContainer.organizationUserApiService,
|
||||||
this.serviceContainer.apiService,
|
this.serviceContainer.apiService,
|
||||||
this.serviceContainer.eventCollectionService,
|
this.serviceContainer.eventCollectionService,
|
||||||
);
|
);
|
||||||
|
@ -412,7 +412,7 @@ export class VaultProgram extends BaseProgram {
|
||||||
const command = new ConfirmCommand(
|
const command = new ConfirmCommand(
|
||||||
this.serviceContainer.apiService,
|
this.serviceContainer.apiService,
|
||||||
this.serviceContainer.cryptoService,
|
this.serviceContainer.cryptoService,
|
||||||
this.serviceContainer.organizationUserService,
|
this.serviceContainer.organizationUserApiService,
|
||||||
);
|
);
|
||||||
const response = await command.run(object, id, cmd);
|
const response = await command.run(object, id, cmd);
|
||||||
this.processResponse(response);
|
this.processResponse(response);
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@bitwarden/common/spec": ["../../libs/common/spec"],
|
"@bitwarden/common/spec": ["../../libs/common/spec"],
|
||||||
|
"@bitwarden/admin-console/common": ["../../libs/admin-console/src/common"],
|
||||||
"@bitwarden/auth/common": ["../../libs/auth/src/common"],
|
"@bitwarden/auth/common": ["../../libs/auth/src/common"],
|
||||||
"@bitwarden/auth/angular": ["../../libs/auth/src/angular"],
|
"@bitwarden/auth/angular": ["../../libs/auth/src/angular"],
|
||||||
"@bitwarden/common/*": ["../../libs/common/src/*"],
|
"@bitwarden/common/*": ["../../libs/common/src/*"],
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"devFlags": {},
|
"devFlags": {},
|
||||||
"flags": {
|
"flags": {
|
||||||
"enableCipherKeyEncryption": true
|
"enableCipherKeyEncryption": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"devFlags": {},
|
"devFlags": {},
|
||||||
"flags": {
|
"flags": {
|
||||||
"enableCipherKeyEncryption": true
|
"enableCipherKeyEncryption": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"flags": {
|
"flags": {
|
||||||
"enableCipherKeyEncryption": true
|
"enableCipherKeyEncryption": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,4 @@ index.node
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
*.node
|
*.node
|
||||||
|
dist
|
||||||
|
|
|
@ -440,9 +440,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cxx"
|
name = "cxx"
|
||||||
version = "1.0.126"
|
version = "1.0.128"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c4eae4b7fc8dcb0032eb3b1beee46b38d371cdeaf2d0c64b9944f6f69ad7755"
|
checksum = "54ccead7d199d584d139148b04b4a368d1ec7556a1d9ea2548febb1b9d49f9a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cxxbridge-flags",
|
"cxxbridge-flags",
|
||||||
|
@ -452,9 +452,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cxx-build"
|
name = "cxx-build"
|
||||||
version = "1.0.126"
|
version = "1.0.128"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c822bf7fb755d97328d6c337120b6f843678178751cba33c9da25cf522272e0"
|
checksum = "c77953e99f01508f89f55c494bfa867171ef3a6c8cea03d26975368f2121a5c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"codespan-reporting",
|
"codespan-reporting",
|
||||||
|
@ -467,21 +467,30 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cxxbridge-flags"
|
name = "cxxbridge-flags"
|
||||||
version = "1.0.126"
|
version = "1.0.128"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "719d6197dc016c88744aff3c0d0340a01ecce12e8939fc282e7c8f583ee64bc6"
|
checksum = "65777e06cc48f0cb0152024c77d6cf9e4bdb4408e7b48bea993d42fa0f5b02b6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cxxbridge-macro"
|
name = "cxxbridge-macro"
|
||||||
version = "1.0.126"
|
version = "1.0.128"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35de3b547387863c8f82013c4f79f1c2162edee956383e4089e1d04c18c4f16c"
|
checksum = "98532a60dedaebc4848cb2cba5023337cc9ea3af16a5b062633fabfd9f18fb60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.3.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive-new"
|
name = "derive-new"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
@ -503,10 +512,14 @@ dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"cbc",
|
"cbc",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
|
"dirs",
|
||||||
|
"futures",
|
||||||
"gio",
|
"gio",
|
||||||
|
"interprocess",
|
||||||
"keytar",
|
"keytar",
|
||||||
"libc",
|
"libc",
|
||||||
"libsecret",
|
"libsecret",
|
||||||
|
"log",
|
||||||
"rand",
|
"rand",
|
||||||
"retry",
|
"retry",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
|
@ -515,6 +528,7 @@ dependencies = [
|
||||||
"sha2",
|
"sha2",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"typenum",
|
"typenum",
|
||||||
"widestring",
|
"widestring",
|
||||||
"windows",
|
"windows",
|
||||||
|
@ -531,6 +545,21 @@ dependencies = [
|
||||||
"napi",
|
"napi",
|
||||||
"napi-build",
|
"napi-build",
|
||||||
"napi-derive",
|
"napi-derive",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "desktop_proxy"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"desktop_core",
|
||||||
|
"futures",
|
||||||
|
"log",
|
||||||
|
"simplelog",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -543,6 +572,27 @@ dependencies = [
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "5.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dlib"
|
name = "dlib"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
@ -552,6 +602,12 @@ dependencies = [
|
||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "doctest-file"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "downcast-rs"
|
name = "downcast-rs"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
@ -646,6 +702,21 @@ version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
|
@ -653,6 +724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -720,6 +792,7 @@ version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
|
@ -896,9 +969,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
|
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
|
@ -914,6 +987,27 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "interprocess"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2f4e4a06d42fab3e85ab1b419ad32b09eab58b901d40c57935ff92db3287a13"
|
||||||
|
dependencies = [
|
||||||
|
"doctest-file",
|
||||||
|
"futures-core",
|
||||||
|
"libc",
|
||||||
|
"recvmsg",
|
||||||
|
"tokio",
|
||||||
|
"widestring",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keytar"
|
name = "keytar"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
@ -951,6 +1045,16 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsecret"
|
name = "libsecret"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -1039,10 +1143,21 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi"
|
name = "mio"
|
||||||
version = "2.16.6"
|
version = "0.8.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfc300228808a0e6aea5a58115c82889240bcf8dab16fc25ad675b33e454b368"
|
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "napi"
|
||||||
|
version = "2.16.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "633e41b2b983cf7983134f0c50986ca524d0caf38a2c6fc893ea3fa2e26abb0c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"ctor",
|
"ctor",
|
||||||
|
@ -1060,9 +1175,9 @@ checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi-derive"
|
name = "napi-derive"
|
||||||
version = "2.16.5"
|
version = "2.16.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e0e034ddf6155192cf83f267ede763fe6c164dfa9971585436b16173718d94c4"
|
checksum = "70a8a778fd367b13c64232e58632514b795514ece491ce136d96e976d34a3eb8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"convert_case",
|
"convert_case",
|
||||||
|
@ -1131,6 +1246,12 @@ dependencies = [
|
||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
|
@ -1141,6 +1262,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_threads"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc-sys"
|
name = "objc-sys"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
|
@ -1242,9 +1372,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.3"
|
version = "0.36.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9"
|
checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
@ -1255,6 +1385,12 @@ version = "1.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-stream"
|
name = "ordered-stream"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -1358,6 +1494,12 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.20"
|
version = "0.2.20"
|
||||||
|
@ -1433,6 +1575,12 @@ dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "recvmsg"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
@ -1442,6 +1590,17 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"libredox",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.6"
|
version = "1.10.6"
|
||||||
|
@ -1623,6 +1782,17 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simplelog"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"termcolor",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
|
@ -1638,6 +1808,16 @@ version = "1.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "static_assertions"
|
name = "static_assertions"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -1646,9 +1826,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.76"
|
version = "2.0.77"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
|
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1716,6 +1896,39 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"itoa",
|
||||||
|
"libc",
|
||||||
|
"num-conv",
|
||||||
|
"num_threads",
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.38.0"
|
version = "1.38.0"
|
||||||
|
@ -1724,9 +1937,13 @@ checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1740,6 +1957,19 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-util"
|
||||||
|
version = "0.7.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.19"
|
version = "0.8.19"
|
||||||
|
@ -2035,6 +2265,15 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.48.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.48.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["napi", "core"]
|
members = ["napi", "core", "proxy"]
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const child_process = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const process = require("process");
|
||||||
|
|
||||||
|
let crossPlatform = process.argv.length > 2 && process.argv[2] === "cross-platform";
|
||||||
|
|
||||||
|
function buildNapiModule(target, release = true) {
|
||||||
|
const targetArg = target ? `--target ${target}` : "";
|
||||||
|
const releaseArg = release ? "--release" : "";
|
||||||
|
return child_process.execSync(`npm run build -- ${releaseArg} ${targetArg}`, { stdio: 'inherit', cwd: path.join(__dirname, "napi") });
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProxyBin(target, release = true) {
|
||||||
|
const targetArg = target ? `--target ${target}` : "";
|
||||||
|
const releaseArg = release ? "--release" : "";
|
||||||
|
return child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!crossPlatform) {
|
||||||
|
console.log("Building native modules in debug mode for the native architecture");
|
||||||
|
buildNapiModule(false, false);
|
||||||
|
buildProxyBin(false, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that targets contains pairs of [rust target, node arch]
|
||||||
|
// We do this to move the output binaries to a location that can
|
||||||
|
// be easily accessed from electron-builder using ${os} and ${arch}
|
||||||
|
let targets = [];
|
||||||
|
switch (process.platform) {
|
||||||
|
case "win32":
|
||||||
|
targets = [
|
||||||
|
["i686-pc-windows-msvc", 'ia32'],
|
||||||
|
["x86_64-pc-windows-msvc", 'x64'],
|
||||||
|
["aarch64-pc-windows-msvc", 'arm64']
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "darwin":
|
||||||
|
targets = [
|
||||||
|
["x86_64-apple-darwin", 'x64'],
|
||||||
|
["aarch64-apple-darwin", 'arm64']
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
targets = [
|
||||||
|
['x86_64-unknown-linux-musl', 'x64']
|
||||||
|
];
|
||||||
|
|
||||||
|
process.env["PKG_CONFIG_ALLOW_CROSS"] = "1";
|
||||||
|
process.env["PKG_CONFIG_ALL_STATIC"] = "1";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Cross building native modules for the targets: ", targets.map(([target, _]) => target).join(", "));
|
||||||
|
|
||||||
|
fs.mkdirSync(path.join(__dirname, "dist"), { recursive: true });
|
||||||
|
|
||||||
|
targets.forEach(([target, nodeArch]) => {
|
||||||
|
buildNapiModule(target);
|
||||||
|
buildProxyBin(target);
|
||||||
|
|
||||||
|
const ext = process.platform === "win32" ? ".exe" : "";
|
||||||
|
fs.copyFileSync(path.join(__dirname, "target", target, "release", `desktop_proxy${ext}`), path.join(__dirname, "dist", `desktop_proxy.${process.platform}-${nodeArch}${ext}`));
|
||||||
|
});
|
|
@ -6,9 +6,21 @@ version = "0.0.0"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["sys"]
|
||||||
manual_test = []
|
manual_test = []
|
||||||
|
|
||||||
|
sys = [
|
||||||
|
"dep:widestring",
|
||||||
|
"dep:windows",
|
||||||
|
"dep:core-foundation",
|
||||||
|
"dep:security-framework",
|
||||||
|
"dep:security-framework-sys",
|
||||||
|
"dep:gio",
|
||||||
|
"dep:libsecret",
|
||||||
|
"dep:zbus",
|
||||||
|
"dep:zbus_polkit",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes = "=0.8.4"
|
aes = "=0.8.4"
|
||||||
anyhow = "=1.0.86"
|
anyhow = "=1.0.86"
|
||||||
|
@ -17,17 +29,22 @@ arboard = { version = "=3.4.0", default-features = false, features = [
|
||||||
] }
|
] }
|
||||||
base64 = "=0.22.1"
|
base64 = "=0.22.1"
|
||||||
cbc = { version = "=0.1.2", features = ["alloc"] }
|
cbc = { version = "=0.1.2", features = ["alloc"] }
|
||||||
|
dirs = "=5.0.1"
|
||||||
|
futures = "=0.3.30"
|
||||||
|
interprocess = { version = "=2.2.1", features = ["tokio"] }
|
||||||
libc = "=0.2.155"
|
libc = "=0.2.155"
|
||||||
|
log = "=0.4.22"
|
||||||
rand = "=0.8.5"
|
rand = "=0.8.5"
|
||||||
retry = "=2.0.0"
|
retry = "=2.0.0"
|
||||||
scopeguard = "=1.2.0"
|
scopeguard = "=1.2.0"
|
||||||
sha2 = "=0.10.8"
|
sha2 = "=0.10.8"
|
||||||
thiserror = "=1.0.61"
|
thiserror = "=1.0.61"
|
||||||
tokio = { version = "=1.38.0", features = ["io-util", "sync", "macros"] }
|
tokio = { version = "=1.38.0", features = ["io-util", "sync", "macros"] }
|
||||||
|
tokio-util = "=0.7.11"
|
||||||
typenum = "=1.17.0"
|
typenum = "=1.17.0"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
widestring = "=1.1.0"
|
widestring = { version = "=1.1.0", optional = true }
|
||||||
windows = { version = "=0.57.0", features = [
|
windows = { version = "=0.57.0", features = [
|
||||||
"Foundation",
|
"Foundation",
|
||||||
"Security_Credentials_UI",
|
"Security_Credentials_UI",
|
||||||
|
@ -38,18 +55,18 @@ windows = { version = "=0.57.0", features = [
|
||||||
"Win32_System_WinRT",
|
"Win32_System_WinRT",
|
||||||
"Win32_UI_Input_KeyboardAndMouse",
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
] }
|
], optional = true }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dev-dependencies]
|
[target.'cfg(windows)'.dev-dependencies]
|
||||||
keytar = "=0.1.6"
|
keytar = "=0.1.6"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
core-foundation = "=0.9.4"
|
core-foundation = { version = "=0.9.4", optional = true }
|
||||||
security-framework = "=2.11.0"
|
security-framework = { version = "=2.11.0", optional = true }
|
||||||
security-framework-sys = "=2.11.0"
|
security-framework-sys = { version = "=2.11.0", optional = true }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
gio = "=0.19.5"
|
gio = { version = "=0.19.5", optional = true }
|
||||||
libsecret = "=0.5.0"
|
libsecret = { version = "=0.5.0", optional = true }
|
||||||
zbus = "=4.3.1"
|
zbus = { version = "=4.3.1", optional = true }
|
||||||
zbus_polkit = "=4.0.0"
|
zbus_polkit = { version = "=4.0.0", optional = true }
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use interprocess::local_socket::{
|
||||||
|
tokio::{prelude::*, Stream},
|
||||||
|
GenericFilePath, ToFsName,
|
||||||
|
};
|
||||||
|
use log::{error, info};
|
||||||
|
use tokio::{
|
||||||
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
|
time::sleep,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::ipc::NATIVE_MESSAGING_BUFFER_SIZE;
|
||||||
|
|
||||||
|
pub async fn connect(
|
||||||
|
path: PathBuf,
|
||||||
|
send: tokio::sync::mpsc::Sender<String>,
|
||||||
|
mut recv: tokio::sync::mpsc::Receiver<String>,
|
||||||
|
) {
|
||||||
|
// Keep track of connection failures to make sure we don't leave the process as a zombie
|
||||||
|
let mut connection_failures = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match connect_inner(&path, &send, &mut recv).await {
|
||||||
|
Ok(()) => return,
|
||||||
|
Err(e) => {
|
||||||
|
connection_failures += 1;
|
||||||
|
if connection_failures >= 20 {
|
||||||
|
error!("Failed to connect to IPC server after 20 attempts: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
error!("Failed to connect to IPC server: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_secs(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_inner(
|
||||||
|
path: &Path,
|
||||||
|
send: &tokio::sync::mpsc::Sender<String>,
|
||||||
|
recv: &mut tokio::sync::mpsc::Receiver<String>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
info!("Attempting to connect to {}", path.display());
|
||||||
|
|
||||||
|
let name = path.as_os_str().to_fs_name::<GenericFilePath>()?;
|
||||||
|
let mut conn = Stream::connect(name).await?;
|
||||||
|
|
||||||
|
info!("Connected to {}", path.display());
|
||||||
|
|
||||||
|
// This `connected` and the latter `disconnected` messages are the only ones that
|
||||||
|
// are sent from the Rust IPC code and not just forwarded from the desktop app.
|
||||||
|
// As it's only two, we hardcode the JSON values to avoid pulling in a JSON library.
|
||||||
|
send.send("{\"command\":\"connected\"}".to_owned()).await?;
|
||||||
|
|
||||||
|
let mut buffer = vec![0; NATIVE_MESSAGING_BUFFER_SIZE];
|
||||||
|
|
||||||
|
// Listen to IPC messages
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
// Forward messages to the IPC server
|
||||||
|
msg = recv.recv() => {
|
||||||
|
match msg {
|
||||||
|
Some(msg) => {
|
||||||
|
conn.write_all(msg.as_bytes()).await?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
info!("Client channel closed");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Forward messages from the IPC server
|
||||||
|
res = conn.read(&mut buffer[..]) => {
|
||||||
|
match res {
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error reading from IPC server: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(0) => {
|
||||||
|
info!("Connection closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(n) => {
|
||||||
|
let message = String::from_utf8_lossy(&buffer[..n]).to_string();
|
||||||
|
send.send(message).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = send.send("{\"command\":\"disconnected\"}".to_owned()).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
pub mod client;
|
||||||
|
pub mod server;
|
||||||
|
|
||||||
|
/// The maximum size of a message that can be sent over IPC.
|
||||||
|
/// According to the documentation, the maximum size sent to the browser is 1MB.
|
||||||
|
/// While the maximum size sent from the browser to the native messaging host is 4GB.
|
||||||
|
///
|
||||||
|
/// Currently we are setting the maximum both ways to be 1MB.
|
||||||
|
///
|
||||||
|
/// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#app_side
|
||||||
|
/// https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging#native-messaging-host-protocol
|
||||||
|
pub const NATIVE_MESSAGING_BUFFER_SIZE: usize = 1024 * 1024;
|
||||||
|
|
||||||
|
/// The maximum number of messages that can be buffered in a channel.
|
||||||
|
/// This number is more or less arbitrary and can be adjusted as needed,
|
||||||
|
/// but ideally the messages should be processed as quickly as possible.
|
||||||
|
pub const MESSAGE_CHANNEL_BUFFER: usize = 32;
|
||||||
|
|
||||||
|
/// Resolve the path to the IPC socket.
|
||||||
|
pub fn path(name: &str) -> std::path::PathBuf {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// Use a unique IPC pipe //./pipe/xxxxxxxxxxxxxxxxx.app.bitwarden per user.
|
||||||
|
// Hashing prevents problems with reserved characters and file length limitations.
|
||||||
|
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||||
|
use sha2::Digest;
|
||||||
|
let home = dirs::home_dir().unwrap();
|
||||||
|
let hash = sha2::Sha256::digest(home.as_os_str().as_encoded_bytes());
|
||||||
|
let hash_b64 = URL_SAFE_NO_PAD.encode(hash.as_slice());
|
||||||
|
|
||||||
|
format!(r"\\.\pipe\{hash_b64}.app.{name}").into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let mut home = dirs::home_dir().unwrap();
|
||||||
|
|
||||||
|
// When running in an unsandboxed environment, path is: /Users/<user>/
|
||||||
|
// While running sandboxed, it's different: /Users/<user>/Library/Containers/com.bitwarden.desktop/Data
|
||||||
|
//
|
||||||
|
// We want to use App Groups in /Users/<user>/Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop,
|
||||||
|
// so we need to remove all the components after the user.
|
||||||
|
// Note that we subtract 3 because the root directory is counted as a component (/, Users, <user>).
|
||||||
|
let num_components = home.components().count();
|
||||||
|
for _ in 0..num_components - 3 {
|
||||||
|
home.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
home.join(format!(
|
||||||
|
"Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop/tmp/app.{name}"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// On Linux, we use the user's cache directory.
|
||||||
|
let home = dirs::cache_dir().unwrap();
|
||||||
|
let path_dir = home.join("com.bitwarden.desktop");
|
||||||
|
|
||||||
|
// The chache directory might not exist, so create it
|
||||||
|
let _ = std::fs::create_dir_all(&path_dir);
|
||||||
|
path_dir.join(format!("app.{name}"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,232 @@
|
||||||
|
use std::{error::Error, path::Path, vec};
|
||||||
|
|
||||||
|
use futures::TryFutureExt;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use interprocess::local_socket::{tokio::prelude::*, GenericFilePath, ListenerOptions};
|
||||||
|
use log::{error, info};
|
||||||
|
use tokio::{
|
||||||
|
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
|
||||||
|
sync::{broadcast, mpsc},
|
||||||
|
};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
use super::{MESSAGE_CHANNEL_BUFFER, NATIVE_MESSAGING_BUFFER_SIZE};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Message {
|
||||||
|
pub client_id: u32,
|
||||||
|
pub kind: MessageType,
|
||||||
|
// This value should be Some for MessageType::Message and None for the rest
|
||||||
|
pub message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MessageType {
|
||||||
|
Connected,
|
||||||
|
Disconnected,
|
||||||
|
Message,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Server {
|
||||||
|
cancel_token: CancellationToken,
|
||||||
|
server_to_clients_send: broadcast::Sender<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
/// Create and start the IPC server without blocking.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// - `name`: The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
|
||||||
|
/// - `client_to_server_send`: This [`mpsc::Sender<Message>`] will receive all the [`Message`]'s that the clients send to this server.
|
||||||
|
pub fn start(
|
||||||
|
path: &Path,
|
||||||
|
client_to_server_send: mpsc::Sender<Message>,
|
||||||
|
) -> Result<Self, Box<dyn Error>> {
|
||||||
|
// If the unix socket file already exists, we get an error when trying to bind to it. So we remove it first.
|
||||||
|
// Any processes that were using the old socket should remain connected to it but any new connections will use the new socket.
|
||||||
|
if !cfg!(windows) {
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = path.as_os_str().to_fs_name::<GenericFilePath>()?;
|
||||||
|
let opts = ListenerOptions::new().name(name);
|
||||||
|
let listener = opts.create_tokio()?;
|
||||||
|
|
||||||
|
// This broadcast channel is used for sending messages to all connected clients, and so the sender
|
||||||
|
// will be stored in the server while the receiver will be cloned and passed to each client handler.
|
||||||
|
let (server_to_clients_send, server_to_clients_recv) =
|
||||||
|
broadcast::channel::<String>(MESSAGE_CHANNEL_BUFFER);
|
||||||
|
|
||||||
|
// This cancellation token allows us to cleanly stop the server and all the spawned
|
||||||
|
// tasks without having to wait on all the pending tasks finalizing first
|
||||||
|
let cancel_token = CancellationToken::new();
|
||||||
|
|
||||||
|
// Create the server and start listening for incoming connections
|
||||||
|
// in a separate task to avoid blocking the current task
|
||||||
|
let server = Server {
|
||||||
|
cancel_token: cancel_token.clone(),
|
||||||
|
server_to_clients_send,
|
||||||
|
};
|
||||||
|
tokio::spawn(listen_incoming(
|
||||||
|
listener,
|
||||||
|
client_to_server_send,
|
||||||
|
server_to_clients_recv,
|
||||||
|
cancel_token,
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a message over the IPC server to all the connected clients
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The number of clients that the message was sent to. Note that the number of messages
|
||||||
|
/// sent may be less than the number of connected clients if some clients disconnect while
|
||||||
|
/// the message is being sent.
|
||||||
|
pub fn send(&self, message: String) -> Result<usize> {
|
||||||
|
let sent = self.server_to_clients_send.send(message)?;
|
||||||
|
Ok(sent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the IPC server.
|
||||||
|
pub fn stop(&self) {
|
||||||
|
self.cancel_token.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Server {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn listen_incoming(
|
||||||
|
listener: LocalSocketListener,
|
||||||
|
client_to_server_send: mpsc::Sender<Message>,
|
||||||
|
server_to_clients_recv: broadcast::Receiver<String>,
|
||||||
|
cancel_token: CancellationToken,
|
||||||
|
) {
|
||||||
|
// We use a simple incrementing ID for each client
|
||||||
|
let mut next_client_id = 1_u32;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel_token.cancelled() => {
|
||||||
|
info!("IPC server cancelled.");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
|
||||||
|
// A new client connection has been established
|
||||||
|
msg = listener.accept() => {
|
||||||
|
match msg {
|
||||||
|
Ok(client_stream) => {
|
||||||
|
let client_id = next_client_id;
|
||||||
|
next_client_id += 1;
|
||||||
|
|
||||||
|
let future = handle_connection(
|
||||||
|
client_stream,
|
||||||
|
client_to_server_send.clone(),
|
||||||
|
// We resubscribe to the receiver here so this task can have it's own copy
|
||||||
|
// Note that this copy will only receive messages sent after this point,
|
||||||
|
// but that is okay, realistically we don't want any messages before we get a chance
|
||||||
|
// to send the connected message to the client, which is done inside [`handle_connection`]
|
||||||
|
server_to_clients_recv.resubscribe(),
|
||||||
|
cancel_token.clone(),
|
||||||
|
client_id
|
||||||
|
);
|
||||||
|
tokio::spawn(future.map_err(|e| {
|
||||||
|
error!("Error handling connection: {}", e)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error accepting connection: {}", e);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_connection(
|
||||||
|
mut client_stream: impl AsyncRead + AsyncWrite + Unpin,
|
||||||
|
client_to_server_send: mpsc::Sender<Message>,
|
||||||
|
mut server_to_clients_recv: broadcast::Receiver<String>,
|
||||||
|
cancel_token: CancellationToken,
|
||||||
|
client_id: u32,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
client_to_server_send
|
||||||
|
.send(Message {
|
||||||
|
client_id,
|
||||||
|
kind: MessageType::Connected,
|
||||||
|
message: None,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut buf = vec![0u8; NATIVE_MESSAGING_BUFFER_SIZE];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel_token.cancelled() => {
|
||||||
|
info!("Client {client_id} cancelled.");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Forward messages to the IPC clients
|
||||||
|
msg = server_to_clients_recv.recv() => {
|
||||||
|
match msg {
|
||||||
|
Ok(msg) => {
|
||||||
|
client_stream.write_all(msg.as_bytes()).await?;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
info!("Error reading message: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Forwards messages from the IPC clients to the server
|
||||||
|
// Note that we also send connect and disconnect events so that
|
||||||
|
// the server can keep track of multiple clients
|
||||||
|
result = client_stream.read(&mut buf) => {
|
||||||
|
match result {
|
||||||
|
Err(e) => {
|
||||||
|
info!("Error reading from client {client_id}: {e}");
|
||||||
|
|
||||||
|
client_to_server_send.send(Message {
|
||||||
|
client_id,
|
||||||
|
kind: MessageType::Disconnected,
|
||||||
|
message: None,
|
||||||
|
}).await?;
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Ok(0) => {
|
||||||
|
info!("Client {client_id} disconnected.");
|
||||||
|
|
||||||
|
client_to_server_send.send(Message {
|
||||||
|
client_id,
|
||||||
|
kind: MessageType::Disconnected,
|
||||||
|
message: None,
|
||||||
|
}).await?;
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Ok(size) => {
|
||||||
|
let msg = std::str::from_utf8(&buf[..size])?;
|
||||||
|
|
||||||
|
client_to_server_send.send(Message {
|
||||||
|
client_id,
|
||||||
|
kind: MessageType::Message,
|
||||||
|
message: Some(msg.to_string()),
|
||||||
|
}).await?;
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,7 +1,13 @@
|
||||||
|
#[cfg(feature = "sys")]
|
||||||
pub mod biometric;
|
pub mod biometric;
|
||||||
|
#[cfg(feature = "sys")]
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod ipc;
|
||||||
|
#[cfg(feature = "sys")]
|
||||||
pub mod password;
|
pub mod password;
|
||||||
|
#[cfg(feature = "sys")]
|
||||||
pub mod process_isolation;
|
pub mod process_isolation;
|
||||||
|
#[cfg(feature = "sys")]
|
||||||
pub mod powermonitor;
|
pub mod powermonitor;
|
||||||
|
|
|
@ -16,8 +16,10 @@ manual_test = []
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "=1.0.86"
|
anyhow = "=1.0.86"
|
||||||
desktop_core = { path = "../core" }
|
desktop_core = { path = "../core" }
|
||||||
napi = { version = "=2.16.6", features = ["async"] }
|
napi = { version = "=2.16.7", features = ["async"] }
|
||||||
napi-derive = "=2.16.5"
|
napi-derive = "=2.16.6"
|
||||||
|
tokio = { version = "1.38.0" }
|
||||||
|
tokio-util = "0.7.11"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
napi-build = "=2.1.3"
|
napi-build = "=2.1.3"
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
||||||
const child_process = require("child_process");
|
|
||||||
const process = require("process");
|
|
||||||
|
|
||||||
let targets = [];
|
|
||||||
switch (process.platform) {
|
|
||||||
case "win32":
|
|
||||||
targets = ["i686-pc-windows-msvc", "x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "darwin":
|
|
||||||
targets = ["x86_64-apple-darwin", "aarch64-apple-darwin"];
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
targets = ['x86_64-unknown-linux-musl'];
|
|
||||||
process.env["PKG_CONFIG_ALLOW_CROSS"] = "1";
|
|
||||||
process.env["PKG_CONFIG_ALL_STATIC"] = "1";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
targets.forEach(target => {
|
|
||||||
child_process.execSync(`npm run build -- --target ${target}`, {stdio: 'inherit'});
|
|
||||||
});
|
|
|
@ -51,3 +51,33 @@ export namespace powermonitors {
|
||||||
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
||||||
export function isLockMonitorAvailable(): Promise<boolean>
|
export function isLockMonitorAvailable(): Promise<boolean>
|
||||||
}
|
}
|
||||||
|
export namespace ipc {
|
||||||
|
export interface IpcMessage {
|
||||||
|
clientId: number
|
||||||
|
kind: IpcMessageType
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
export const enum IpcMessageType {
|
||||||
|
Connected = 0,
|
||||||
|
Disconnected = 1,
|
||||||
|
Message = 2
|
||||||
|
}
|
||||||
|
export class IpcServer {
|
||||||
|
/**
|
||||||
|
* Create and start the IPC server without blocking.
|
||||||
|
*
|
||||||
|
* @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
|
||||||
|
* @param callback This function will be called whenever a message is received from a client.
|
||||||
|
*/
|
||||||
|
static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise<IpcServer>
|
||||||
|
/** Stop the IPC server. */
|
||||||
|
stop(): void
|
||||||
|
/**
|
||||||
|
* Send a message over the IPC server to all the connected clients
|
||||||
|
*
|
||||||
|
* @return The number of clients that the message was sent to. Note that the number of messages
|
||||||
|
* actually received may be less, as some clients could disconnect before receiving the message.
|
||||||
|
*/
|
||||||
|
send(message: string): number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -206,10 +206,4 @@ if (!nativeBinding) {
|
||||||
throw new Error(`Failed to load native binding`)
|
throw new Error(`Failed to load native binding`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { passwords, biometrics, clipboards, processisolations, powermonitors } = nativeBinding
|
module.exports = nativeBinding
|
||||||
|
|
||||||
module.exports.passwords = passwords
|
|
||||||
module.exports.biometrics = biometrics
|
|
||||||
module.exports.clipboards = clipboards
|
|
||||||
module.exports.processisolations = processisolations
|
|
||||||
module.exports.powermonitors = powermonitors
|
|
||||||
|
|
|
@ -3,9 +3,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "napi build --release --platform --js false",
|
"build": "napi build --platform --js false",
|
||||||
"build:debug": "napi build --platform --js false",
|
|
||||||
"build:cross-platform": "node build.js",
|
|
||||||
"test": "cargo test"
|
"test": "cargo test"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|
|
@ -189,3 +189,103 @@ pub mod powermonitors {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub mod ipc {
|
||||||
|
use desktop_core::ipc::server::{Message, MessageType};
|
||||||
|
use napi::threadsafe_function::{
|
||||||
|
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct IpcMessage {
|
||||||
|
pub client_id: u32,
|
||||||
|
pub kind: IpcMessageType,
|
||||||
|
pub message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Message> for IpcMessage {
|
||||||
|
fn from(message: Message) -> Self {
|
||||||
|
IpcMessage {
|
||||||
|
client_id: message.client_id,
|
||||||
|
kind: message.kind.into(),
|
||||||
|
message: message.message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub enum IpcMessageType {
|
||||||
|
Connected,
|
||||||
|
Disconnected,
|
||||||
|
Message,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MessageType> for IpcMessageType {
|
||||||
|
fn from(message_type: MessageType) -> Self {
|
||||||
|
match message_type {
|
||||||
|
MessageType::Connected => IpcMessageType::Connected,
|
||||||
|
MessageType::Disconnected => IpcMessageType::Disconnected,
|
||||||
|
MessageType::Message => IpcMessageType::Message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub struct IpcServer {
|
||||||
|
server: desktop_core::ipc::server::Server,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl IpcServer {
|
||||||
|
/// Create and start the IPC server without blocking.
|
||||||
|
///
|
||||||
|
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
|
||||||
|
/// @param callback This function will be called whenever a message is received from a client.
|
||||||
|
#[napi(factory)]
|
||||||
|
pub async fn listen(
|
||||||
|
name: String,
|
||||||
|
#[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")]
|
||||||
|
callback: ThreadsafeFunction<IpcMessage, ErrorStrategy::CalleeHandled>,
|
||||||
|
) -> napi::Result<Self> {
|
||||||
|
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(message) = recv.recv().await {
|
||||||
|
callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let path = desktop_core::ipc::path(&name);
|
||||||
|
|
||||||
|
let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| {
|
||||||
|
napi::Error::from_reason(format!(
|
||||||
|
"Error listening to server - Path: {path:?} - Error: {e} - {e:?}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(IpcServer { server })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the IPC server.
|
||||||
|
#[napi]
|
||||||
|
pub fn stop(&self) -> napi::Result<()> {
|
||||||
|
self.server.stop();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a message over the IPC server to all the connected clients
|
||||||
|
///
|
||||||
|
/// @return The number of clients that the message was sent to. Note that the number of messages
|
||||||
|
/// actually received may be less, as some clients could disconnect before receiving the message.
|
||||||
|
#[napi]
|
||||||
|
pub fn send(&self, message: String) -> napi::Result<u32> {
|
||||||
|
self.server
|
||||||
|
.send(message)
|
||||||
|
.map_err(|e| {
|
||||||
|
napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}"))
|
||||||
|
})
|
||||||
|
// NAPI doesn't support u64 or usize, so we need to convert to u32
|
||||||
|
.map(|u| u32::try_from(u).unwrap_or_default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
edition = "2021"
|
||||||
|
exclude = ["index.node"]
|
||||||
|
license = "GPL-3.0"
|
||||||
|
name = "desktop_proxy"
|
||||||
|
version = "0.0.0"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "=1.0.86"
|
||||||
|
desktop_core = { path = "../core", default-features = false }
|
||||||
|
futures = "0.3.30"
|
||||||
|
log = "0.4.21"
|
||||||
|
simplelog = "0.12.2"
|
||||||
|
tokio = { version = "1.38.0", features = ["io-std", "io-util", "macros", "rt"] }
|
||||||
|
tokio-util = { version = "0.7.11", features = ["codec"] }
|
|
@ -0,0 +1,137 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use desktop_core::ipc::{MESSAGE_CHANNEL_BUFFER, NATIVE_MESSAGING_BUFFER_SIZE};
|
||||||
|
use futures::{SinkExt, StreamExt};
|
||||||
|
use log::*;
|
||||||
|
use tokio_util::codec::LengthDelimitedCodec;
|
||||||
|
|
||||||
|
fn init_logging(log_path: &Path, level: log::LevelFilter) {
|
||||||
|
use simplelog::{ColorChoice, CombinedLogger, Config, SharedLogger, TermLogger, TerminalMode};
|
||||||
|
|
||||||
|
let config = Config::default();
|
||||||
|
|
||||||
|
let mut loggers: Vec<Box<dyn SharedLogger>> = Vec::new();
|
||||||
|
loggers.push(TermLogger::new(
|
||||||
|
level,
|
||||||
|
config.clone(),
|
||||||
|
TerminalMode::Stderr,
|
||||||
|
ColorChoice::Auto,
|
||||||
|
));
|
||||||
|
|
||||||
|
match std::fs::File::create(log_path) {
|
||||||
|
Ok(file) => {
|
||||||
|
loggers.push(simplelog::WriteLogger::new(level, config, file));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Can't create file: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = CombinedLogger::init(loggers) {
|
||||||
|
eprintln!("Failed to initialize logger: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bitwarden IPC Proxy.
|
||||||
|
///
|
||||||
|
/// This proxy allows browser extensions to communicate with a desktop application using Native
|
||||||
|
/// Messaging. This method allows an extension to send and receive messages through the use of
|
||||||
|
/// stdin/stdout streams.
|
||||||
|
///
|
||||||
|
/// However, this also requires the browser to start the process in order for the communication to
|
||||||
|
/// occur. To overcome this limitation, we implement Inter-Process Communication (IPC) to establish
|
||||||
|
/// a stable communication channel between the proxy and the running desktop application.
|
||||||
|
///
|
||||||
|
/// Browser extension <-[native messaging]-> proxy <-[ipc]-> desktop
|
||||||
|
///
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() {
|
||||||
|
let sock_path = desktop_core::ipc::path("bitwarden");
|
||||||
|
|
||||||
|
let log_path = {
|
||||||
|
let mut path = sock_path.clone();
|
||||||
|
path.set_extension("bitwarden.log");
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
init_logging(&log_path, LevelFilter::Info);
|
||||||
|
|
||||||
|
info!("Starting Bitwarden IPC Proxy.");
|
||||||
|
|
||||||
|
// Different browsers send different arguments when the app starts:
|
||||||
|
//
|
||||||
|
// Firefox:
|
||||||
|
// - The complete path to the app manifest. (in the form `/Users/<user>/Library/.../Mozilla/NativeMessagingHosts/com.8bit.bitwarden.json`)
|
||||||
|
// - (in Firefox 55+) the ID (as given in the manifest.json) of the add-on that started it (in the form `{[UUID]}`).
|
||||||
|
//
|
||||||
|
// Chrome on Windows:
|
||||||
|
// - Origin of the extension that started it (in the form `chrome-extension://[ID]`).
|
||||||
|
// - Handle to the Chrome native window that started the app.
|
||||||
|
//
|
||||||
|
// Chrome on Linux and Mac:
|
||||||
|
// - Origin of the extension that started it (in the form `chrome-extension://[ID]`).
|
||||||
|
|
||||||
|
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||||
|
info!("Process args: {:?}", args);
|
||||||
|
|
||||||
|
// Setup two channels, one for sending messages to the desktop application (`out`) and one for receiving messages from the desktop application (`in`)
|
||||||
|
let (in_send, in_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER);
|
||||||
|
let (out_send, mut out_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER);
|
||||||
|
|
||||||
|
let mut handle = tokio::spawn(desktop_core::ipc::client::connect(
|
||||||
|
sock_path, out_send, in_recv,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Create a new codec for reading and writing messages from stdin/stdout.
|
||||||
|
let mut stdin = LengthDelimitedCodec::builder()
|
||||||
|
.max_frame_length(NATIVE_MESSAGING_BUFFER_SIZE)
|
||||||
|
.native_endian()
|
||||||
|
.new_read(tokio::io::stdin());
|
||||||
|
let mut stdout = LengthDelimitedCodec::builder()
|
||||||
|
.max_frame_length(NATIVE_MESSAGING_BUFFER_SIZE)
|
||||||
|
.native_endian()
|
||||||
|
.new_write(tokio::io::stdout());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
// IPC client has finished, so we should exit as well.
|
||||||
|
_ = &mut handle => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive messages from IPC and print to STDOUT.
|
||||||
|
msg = out_recv.recv() => {
|
||||||
|
match msg {
|
||||||
|
Some(msg) => {
|
||||||
|
debug!("OUT: {}", msg);
|
||||||
|
stdout.send(msg.into()).await.unwrap();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
info!("Channel closed, exiting.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Listen to stdin and send messages to ipc processor.
|
||||||
|
msg = stdin.next() => {
|
||||||
|
match msg {
|
||||||
|
Some(Ok(msg)) => {
|
||||||
|
let m = String::from_utf8(msg.to_vec()).unwrap();
|
||||||
|
debug!("IN: {}", m);
|
||||||
|
in_send.send(m).await.unwrap();
|
||||||
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
error!("Error parsing input: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
info!("Received EOF, exiting.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,6 +73,13 @@
|
||||||
"CFBundleDevelopmentRegion": "en"
|
"CFBundleDevelopmentRegion": "en"
|
||||||
},
|
},
|
||||||
"singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node",
|
"singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node",
|
||||||
|
"extraFiles": [
|
||||||
|
{
|
||||||
|
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}",
|
||||||
|
"to": "MacOS/desktop_proxy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"signIgnore": ["MacOS/desktop_proxy"],
|
||||||
"target": ["dmg", "zip"]
|
"target": ["dmg", "zip"]
|
||||||
},
|
},
|
||||||
"win": {
|
"win": {
|
||||||
|
@ -84,16 +91,24 @@
|
||||||
"from": "../../node_modules/regedit/vbs",
|
"from": "../../node_modules/regedit/vbs",
|
||||||
"to": "regedit/vbs",
|
"to": "regedit/vbs",
|
||||||
"filter": ["**/*"]
|
"filter": ["**/*"]
|
||||||
},
|
}
|
||||||
|
],
|
||||||
|
"extraFiles": [
|
||||||
{
|
{
|
||||||
"from": "resources/native-messaging.bat",
|
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe",
|
||||||
"to": "native-messaging.bat"
|
"to": "desktop_proxy.exe"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"category": "Utility",
|
"category": "Utility",
|
||||||
"synopsis": "A secure and free password manager for all of your devices.",
|
"synopsis": "A secure and free password manager for all of your devices.",
|
||||||
|
"extraFiles": [
|
||||||
|
{
|
||||||
|
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}",
|
||||||
|
"to": "desktop_proxy"
|
||||||
|
}
|
||||||
|
],
|
||||||
"target": ["deb", "freebsd", "rpm", "AppImage", "snap"],
|
"target": ["deb", "freebsd", "rpm", "AppImage", "snap"],
|
||||||
"desktop": {
|
"desktop": {
|
||||||
"Name": "Bitwarden",
|
"Name": "Bitwarden",
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
"yargs": "17.7.2"
|
"yargs": "17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.16.1",
|
"@types/node": "20.16.4",
|
||||||
"@types/node-ipc": "9.2.3",
|
"@types/node-ipc": "9.2.3",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4"
|
||||||
}
|
}
|
||||||
|
@ -106,9 +106,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.16.1",
|
"version": "20.16.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.4.tgz",
|
||||||
"integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==",
|
"integrity": "sha512-ioyQ1zK9aGEomJ45zz8S8IdzElyxhvP1RVWnPrXDf6wFaUb+kk1tEcVVJkF7RPGM0VWI7cp5U57oCPIn5iN1qg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
|
@ -241,9 +241,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.1.2",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
|
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
"yargs": "17.7.2"
|
"yargs": "17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.16.1",
|
"@types/node": "20.16.4",
|
||||||
"@types/node-ipc": "9.2.3",
|
"@types/node-ipc": "9.2.3",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@bitwarden/desktop",
|
"name": "@bitwarden/desktop",
|
||||||
"description": "A secure and free password manager for all of your devices.",
|
"description": "A secure and free password manager for all of your devices.",
|
||||||
"version": "2024.8.3",
|
"version": "2024.9.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"bitwarden",
|
"bitwarden",
|
||||||
"password",
|
"password",
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "electron-rebuild",
|
"postinstall": "electron-rebuild",
|
||||||
"start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build",
|
"start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build",
|
||||||
"build-native": "cd desktop_native/napi && npm run build",
|
"build-native": "cd desktop_native && node build.js",
|
||||||
"build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"",
|
"build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"",
|
||||||
"build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"",
|
"build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"",
|
||||||
"build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js",
|
"build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js",
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.application-identifier</key>
|
||||||
|
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||||
|
<key>com.apple.developer.team-identifier</key>
|
||||||
|
<string>LTZ2PFU5D6</string>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -8,6 +8,10 @@
|
||||||
<string>LTZ2PFU5D6</string>
|
<string>LTZ2PFU5D6</string>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||||
|
</array>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
@echo off
|
|
||||||
:: Helper script for starting the Native Messaging Proxy on Windows.
|
|
||||||
|
|
||||||
cd ../
|
|
||||||
set ELECTRON_RUN_AS_NODE=1
|
|
||||||
set ELECTRON_NO_ATTACH_CONSOLE=1
|
|
||||||
Bitwarden.exe resources/app.asar %*
|
|
|
@ -1,14 +1,22 @@
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
|
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
const child_process = require("child_process");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
|
const { flipFuses, FuseVersion, FuseV1Options } = require("@electron/fuses");
|
||||||
|
const builder = require("electron-builder");
|
||||||
const fse = require("fs-extra");
|
const fse = require("fs-extra");
|
||||||
|
|
||||||
exports.default = run;
|
exports.default = run;
|
||||||
|
|
||||||
async function run(context) {
|
async function run(context) {
|
||||||
console.log("## After pack");
|
console.log("## After pack");
|
||||||
console.log(context);
|
// console.log(context);
|
||||||
|
|
||||||
|
if (context.packager.platform.nodeName !== "darwin" || context.arch === builder.Arch.universal) {
|
||||||
|
await addElectronFuses(context);
|
||||||
|
}
|
||||||
|
|
||||||
if (context.electronPlatformName === "linux") {
|
if (context.electronPlatformName === "linux") {
|
||||||
console.log("Creating memory-protection wrapper script");
|
console.log("Creating memory-protection wrapper script");
|
||||||
const appOutDir = context.appOutDir;
|
const appOutDir = context.appOutDir;
|
||||||
|
@ -23,4 +31,114 @@ async function run(context) {
|
||||||
fse.chmodSync(wrapperBin, "755");
|
fse.chmodSync(wrapperBin, "755");
|
||||||
console.log("Copied memory-protection wrapper script");
|
console.log("Copied memory-protection wrapper script");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (["darwin", "mas"].includes(context.electronPlatformName)) {
|
||||||
|
const identities = getIdentities(process.env.CSC_NAME ?? "");
|
||||||
|
if (identities.length === 0) {
|
||||||
|
throw new Error("No valid identities found");
|
||||||
|
}
|
||||||
|
const id = identities[0].id;
|
||||||
|
|
||||||
|
console.log("Signing proxy binary before the main bundle, using identity", id);
|
||||||
|
|
||||||
|
const appName = context.packager.appInfo.productFilename;
|
||||||
|
const appPath = `${context.appOutDir}/${appName}.app`;
|
||||||
|
const proxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy");
|
||||||
|
|
||||||
|
const packageId = "com.bitwarden.desktop";
|
||||||
|
const entitlementsName = "entitlements.desktop_proxy.plist";
|
||||||
|
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
|
||||||
|
child_process.execSync(
|
||||||
|
`codesign -s ${id} -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partially based on electron-builder code:
|
||||||
|
// https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/macPackager.ts
|
||||||
|
// https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/codeSign/macCodeSign.ts
|
||||||
|
|
||||||
|
const appleCertificatePrefixes = [
|
||||||
|
"Developer ID Application:",
|
||||||
|
// "Developer ID Installer:",
|
||||||
|
// "3rd Party Mac Developer Application:",
|
||||||
|
// "3rd Party Mac Developer Installer:",
|
||||||
|
"Apple Development:",
|
||||||
|
];
|
||||||
|
|
||||||
|
function getIdentities(csc_name) {
|
||||||
|
const ids = child_process
|
||||||
|
.execSync("/usr/bin/security find-identity -v -p codesigning")
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
return ids
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => {
|
||||||
|
for (const prefix of appleCertificatePrefixes) {
|
||||||
|
if (line.includes(prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.filter((line) => line.includes(csc_name))
|
||||||
|
.map((line) => {
|
||||||
|
const split = line.trim().split(" ");
|
||||||
|
const id = split[1];
|
||||||
|
const name = split.slice(2).join(" ").replace(/"/g, "");
|
||||||
|
return { id, name };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("electron-builder").AfterPackContext} context
|
||||||
|
*/
|
||||||
|
async function addElectronFuses(context) {
|
||||||
|
const platform = context.packager.platform.nodeName;
|
||||||
|
|
||||||
|
const ext = {
|
||||||
|
darwin: ".app",
|
||||||
|
win32: ".exe",
|
||||||
|
linux: "",
|
||||||
|
}[platform];
|
||||||
|
|
||||||
|
const IS_LINUX = platform === "linux";
|
||||||
|
const executableName = IS_LINUX
|
||||||
|
? context.packager.appInfo.productFilename.toLowerCase().replace("-dev", "").replace(" ", "-")
|
||||||
|
: context.packager.appInfo.productFilename; // .toLowerCase() to accomodate Linux file named `name` but productFileName is `Name` -- Replaces '-dev' because on Linux the executable name is `name` even for the DEV builds
|
||||||
|
|
||||||
|
const electronBinaryPath = path.join(context.appOutDir, `${executableName}${ext}`);
|
||||||
|
|
||||||
|
console.log("## Adding fuses to the electron binary", electronBinaryPath);
|
||||||
|
|
||||||
|
await flipFuses(electronBinaryPath, {
|
||||||
|
version: FuseVersion.V1,
|
||||||
|
strictlyRequireAllFuses: true,
|
||||||
|
resetAdHocDarwinSignature: platform === "darwin" && context.arch === builder.Arch.universal,
|
||||||
|
|
||||||
|
// List of fuses and their default values is available at:
|
||||||
|
// https://www.electronjs.org/docs/latest/tutorial/fuses
|
||||||
|
|
||||||
|
[FuseV1Options.RunAsNode]: false,
|
||||||
|
[FuseV1Options.EnableCookieEncryption]: true,
|
||||||
|
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||||
|
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||||
|
|
||||||
|
// Currently, asar integrity is only implemented for macOS and Windows
|
||||||
|
// https://www.electronjs.org/docs/latest/tutorial/asar-integrity
|
||||||
|
// On macOS, it works by default, but on Windows it requires the
|
||||||
|
// asarIntegrity feature of electron-builder v25, currently in alpha
|
||||||
|
// https://github.com/electron-userland/electron-builder/releases/tag/v25.0.0-alpha.10
|
||||||
|
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: platform === "darwin",
|
||||||
|
|
||||||
|
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||||
|
|
||||||
|
// App refuses to open when enabled
|
||||||
|
[FuseV1Options.LoadBrowserProcessSpecificV8Snapshot]: false,
|
||||||
|
|
||||||
|
// To disable this, we should stop using the file:// protocol to load the app bundle
|
||||||
|
// This can be done by defining a custom app:// protocol and loading the bundle from there,
|
||||||
|
// but then any requests to the server will be blocked by CORS policy
|
||||||
|
[FuseV1Options.GrantFileProtocolExtraPrivileges]: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||||
import { Component } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
|
import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ type InactiveAccount = ActiveAccount & {
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AccountSwitcherComponent {
|
export class AccountSwitcherComponent implements OnInit {
|
||||||
activeAccount$: Observable<ActiveAccount | null>;
|
activeAccount$: Observable<ActiveAccount | null>;
|
||||||
inactiveAccounts$: Observable<{ [userId: string]: InactiveAccount }>;
|
inactiveAccounts$: Observable<{ [userId: string]: InactiveAccount }>;
|
||||||
authStatus = AuthenticationStatus;
|
authStatus = AuthenticationStatus;
|
||||||
|
@ -151,6 +151,24 @@ export class AccountSwitcherComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
const active = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
if (active == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const authStatus = await firstValueFrom(
|
||||||
|
this.authService.authStatuses$.pipe(map((statuses) => statuses[active.id])),
|
||||||
|
);
|
||||||
|
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||||
|
const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$);
|
||||||
|
if (nextUpAccount != null) {
|
||||||
|
await this.switch(nextUpAccount.id);
|
||||||
|
} else {
|
||||||
|
await this.addAccount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.isOpen = !this.isOpen;
|
this.isOpen = !this.isOpen;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { APP_INITIALIZER, NgModule } from "@angular/core";
|
import { APP_INITIALIZER, NgModule } from "@angular/core";
|
||||||
import { Subject, merge } from "rxjs";
|
import { Subject, merge } from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||||
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||||
import {
|
import {
|
||||||
SECURE_STORAGE,
|
SECURE_STORAGE,
|
||||||
|
@ -26,7 +27,6 @@ import {
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
|
||||||
import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
@ -285,7 +285,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
KdfConfigService,
|
KdfConfigService,
|
||||||
InternalMasterPasswordServiceAbstraction,
|
InternalMasterPasswordServiceAbstraction,
|
||||||
OrganizationApiServiceAbstraction,
|
OrganizationApiServiceAbstraction,
|
||||||
OrganizationUserService,
|
OrganizationUserApiService,
|
||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -79,9 +79,10 @@ export class LoginApprovalComponent implements OnInit, OnDestroy {
|
||||||
this.email = await await firstValueFrom(
|
this.email = await await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||||
);
|
);
|
||||||
this.fingerprintPhrase = (
|
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
||||||
await this.cryptoService.getFingerprint(this.email, publicKey)
|
this.email,
|
||||||
).join("-");
|
publicKey,
|
||||||
|
);
|
||||||
this.updateTimeText();
|
this.updateTimeText();
|
||||||
|
|
||||||
this.interval = setInterval(() => {
|
this.interval = setInterval(() => {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||||
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
|
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
@ -50,7 +50,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
stateService: StateService,
|
stateService: StateService,
|
||||||
organizationApiService: OrganizationApiServiceAbstraction,
|
organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
organizationUserService: OrganizationUserService,
|
organizationUserApiService: OrganizationUserApiService,
|
||||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
ssoLoginService: SsoLoginServiceAbstraction,
|
ssoLoginService: SsoLoginServiceAbstraction,
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
|
@ -74,7 +74,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
|
||||||
route,
|
route,
|
||||||
stateService,
|
stateService,
|
||||||
organizationApiService,
|
organizationApiService,
|
||||||
organizationUserService,
|
organizationUserApiService,
|
||||||
userDecryptionOptionsService,
|
userDecryptionOptionsService,
|
||||||
ssoLoginService,
|
ssoLoginService,
|
||||||
dialogService,
|
dialogService,
|
||||||
|
|
|
@ -1,31 +1,33 @@
|
||||||
import { NativeMessagingProxy } from "./proxy/native-messaging-proxy";
|
import { spawn } from "child_process";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
// We need to import the other dependencies using `require` since `import` will
|
import { app } from "electron";
|
||||||
// generate `Error: Cannot find module 'electron'`. The cause of this error is
|
|
||||||
// due to native messaging setting the ELECTRON_RUN_AS_NODE env flag on windows
|
|
||||||
// which removes the electron module. This flag is needed for stdin/out to work
|
|
||||||
// properly on Windows.
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
process.platform === "darwin" &&
|
||||||
process.argv.some((arg) => arg.indexOf("chrome-extension://") !== -1 || arg.indexOf("{") !== -1)
|
process.argv.some((arg) => arg.indexOf("chrome-extension://") !== -1 || arg.indexOf("{") !== -1)
|
||||||
) {
|
) {
|
||||||
if (process.platform === "darwin") {
|
// If we're on MacOS, we need to support DuckDuckGo's IPC communication,
|
||||||
// eslint-disable-next-line
|
// which for the moment is launching the Bitwarden process.
|
||||||
const app = require("electron").app;
|
// Ideally the browser would instead startup the desktop_proxy process
|
||||||
|
// when available, but for now we'll just launch it here.
|
||||||
|
|
||||||
app.on("ready", () => {
|
app.on("ready", () => {
|
||||||
app.dock.hide();
|
app.dock.hide();
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdout.on("error", (e) => {
|
|
||||||
if (e.code === "EPIPE") {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const proxy = new NativeMessagingProxy();
|
const proc = spawn(path.join(process.execPath, "..", "desktop_proxy"), process.argv.slice(1), {
|
||||||
proxy.run();
|
cwd: process.cwd(),
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("exit", () => {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
proc.on("error", () => {
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const Main = require("./main").Main;
|
const Main = require("./main").Main;
|
||||||
|
|
|
@ -220,6 +220,7 @@ export class Main {
|
||||||
this.windowMain,
|
this.windowMain,
|
||||||
app.getPath("userData"),
|
app.getPath("userData"),
|
||||||
app.getPath("exe"),
|
app.getPath("exe"),
|
||||||
|
app.getAppPath(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider);
|
this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider);
|
||||||
|
@ -265,13 +266,21 @@ export class Main {
|
||||||
if (browserIntegrationEnabled || ddgIntegrationEnabled) {
|
if (browserIntegrationEnabled || ddgIntegrationEnabled) {
|
||||||
// Re-register the native messaging host integrations on startup, in case they are not present
|
// Re-register the native messaging host integrations on startup, in case they are not present
|
||||||
if (browserIntegrationEnabled) {
|
if (browserIntegrationEnabled) {
|
||||||
this.nativeMessagingMain.generateManifests().catch(this.logService.error);
|
this.nativeMessagingMain
|
||||||
|
.generateManifests()
|
||||||
|
.catch((err) => this.logService.error("Error while generating manifests", err));
|
||||||
}
|
}
|
||||||
if (ddgIntegrationEnabled) {
|
if (ddgIntegrationEnabled) {
|
||||||
this.nativeMessagingMain.generateDdgManifests().catch(this.logService.error);
|
this.nativeMessagingMain
|
||||||
|
.generateDdgManifests()
|
||||||
|
.catch((err) => this.logService.error("Error while generating DDG manifests", err));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.nativeMessagingMain.listen();
|
this.nativeMessagingMain
|
||||||
|
.listen()
|
||||||
|
.catch((err) =>
|
||||||
|
this.logService.error("Error while starting native message listener", err),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.removeAsDefaultProtocolClient("bitwarden");
|
app.removeAsDefaultProtocolClient("bitwarden");
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue