diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5ba5885d725..c050ee1f6c0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -71,6 +71,7 @@ bitwarden_license/bit-web/src/app/billing @bitwarden/team-billing-dev ## Platform team files ## apps/browser/src/platform @bitwarden/team-platform-dev apps/cli/src/platform @bitwarden/team-platform-dev +apps/desktop/macos @bitwarden/team-platform-dev apps/desktop/src/platform @bitwarden/team-platform-dev apps/web/src/app/platform @bitwarden/team-platform-dev libs/angular/src/platform @bitwarden/team-platform-dev @@ -91,6 +92,7 @@ apps/web/src/translation-constants.ts @bitwarden/team-platform-dev apps/browser/src/autofill @bitwarden/team-autofill-dev apps/desktop/src/autofill @bitwarden/team-autofill-dev libs/common/src/autofill @bitwarden/team-autofill-dev +apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev # DuckDuckGo integration apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev apps/desktop/src/services/native-message-handler.service.ts @bitwarden/team-autofill-dev @@ -122,6 +124,9 @@ apps/cli/src/locales/en/messages.json apps/desktop/src/locales/en/messages.json apps/web/src/locales/en/messages.json +## Ssh agent temporary co-codeowner +apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-platform-dev @bitwarden/wg-ssh-keys + ## BRE team owns these workflows ## .github/workflows/brew-bump-desktop.yml @bitwarden/dept-bre .github/workflows/deploy-web.yml @bitwarden/dept-bre diff --git a/.github/renovate.json b/.github/renovate.json index 562622807c2..0172403f0f1 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -35,18 +35,18 @@ "matchPackageNames": ["@types/jest", "jest", "ts-jest", "jest-preset-angular"], "matchUpdateTypes": "major" }, + { + "groupName": "macOS/iOS bindings", + "matchPackageNames": ["core-foundation", "security-framework", "security-framework-sys"] + }, { "matchPackageNames": [ - "@ngtools/webpack", "base64-loader", "buffer", "bufferutil", - "copy-webpack-plugin", "core-js", "css-loader", "html-loader", - "html-webpack-injector", - "html-webpack-plugin", "mini-css-extract-plugin", "ngx-infinite-scroll", "postcss", @@ -56,20 +56,15 @@ "sass-loader", "style-loader", "ts-loader", - "tsconfig-paths-webpack-plugin", "url", - "util", - "webpack", - "webpack-cli", - "webpack-dev-server", - "webpack-node-externals" + "util" ], "description": "Admin Console owned dependencies", "commitMessagePrefix": "[deps] AC:", "reviewers": ["team:team-admin-console-dev"] }, { - "matchPackageNames": ["@types/node-ipc", "node-ipc", "qrious", "regedit"], + "matchPackageNames": ["qrious"], "description": "Auth owned dependencies", "commitMessagePrefix": "[deps] Auth:", "reviewers": ["team:team-auth-dev"] @@ -106,19 +101,43 @@ }, { "matchPackageNames": [ - "@types/argon2-browser", + "@babel/core", + "@babel/preset-env", + "@electron/notarize", + "@electron/rebuild", + "@ngtools/webpack", "@types/chrome", "@types/firefox-webext-browser", + "@types/glob", "@types/jquery", + "@types/lowdb", "@types/node", "@types/node-forge", - "argon2", - "argon2-browser", - "big-integer", + "@types/node-ipc", + "@yao-pkg", + "babel-loader", + "browserslist", + "copy-webpack-plugin", + "electron", + "electron-builder", + "electron-log", + "electron-reload", + "electron-store", + "electron-updater", + "html-webpack-injector", + "html-webpack-plugin", + "lowdb", "node-forge", + "node-ipc", + "pkg", "rxjs", + "tsconfig-paths-webpack-plugin", "type-fest", - "typescript" + "typescript", + "webpack", + "webpack-cli", + "webpack-dev-server", + "webpack-node-externals" ], "description": "Platform owned dependencies", "commitMessagePrefix": "[deps] Platform:", @@ -193,19 +212,11 @@ }, { "matchPackageNames": [ - "@electron/notarize", - "@electron/rebuild", "@microsoft/signalr-protocol-msgpack", "@microsoft/signalr", "@types/jsdom", "@types/papaparse", "@types/zxcvbn", - "electron-builder", - "electron-log", - "electron-reload", - "electron-store", - "electron-updater", - "electron", "jsdom", "jszip", "oidc-client-ts", @@ -227,7 +238,6 @@ "@types/koa__router", "@types/koa-bodyparser", "@types/koa-json", - "@types/lowdb", "@types/lunr", "@types/node-fetch", "@types/proper-lockfile", @@ -240,19 +250,23 @@ "koa", "koa-bodyparser", "koa-json", - "lowdb", "lunr", "multer", "node-fetch", "open", - "pkg", "proper-lockfile", "qrcode-parser" ], "description": "Vault owned dependencies", "commitMessagePrefix": "[deps] Vault:", "reviewers": ["team:team-vault-dev"] + }, + { + "matchPackageNames": ["@types/argon2-browser", "argon2", "argon2-browser", "big-integer"], + "description": "Key Management owned dependencies", + "commitMessagePrefix": "[deps] KM:", + "reviewers": ["team:team-key-management-dev"] } ], - "ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm", "regedit"] + "ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"] } diff --git a/.github/workflows/auto-branch-updater.yml b/.github/workflows/auto-branch-updater.yml index 97f020fde7b..dc4a43fc34e 100644 --- a/.github/workflows/auto-branch-updater.yml +++ b/.github/workflows/auto-branch-updater.yml @@ -28,7 +28,7 @@ jobs: run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: 'eu-web-${{ steps.setup.outputs.branch }}' fetch-depth: 0 diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 20a36dc5b23..42d012d5a98 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -1,7 +1,8 @@ name: Build Browser on: - pull_request: + pull_request_target: + types: [opened, synchronize] branches-ignore: - 'l10n_master' - 'cf-pages' @@ -33,16 +34,24 @@ defaults: shell: bash jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: repo_url: ${{ steps.gen_vars.outputs.repo_url }} adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Get Package Version id: gen_vars @@ -71,8 +80,10 @@ jobs: run: working-directory: apps/browser steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Testing locales - extName length run: | @@ -109,11 +120,13 @@ jobs: _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -163,10 +176,6 @@ jobs: run: npm run dist:mv3 working-directory: browser-source/apps/browser - - name: Build Chrome Manifest v3 Beta - run: npm run dist:chrome:beta - working-directory: browser-source/apps/browser - - name: Upload Opera artifact uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: @@ -188,13 +197,6 @@ jobs: path: browser-source/apps/browser/dist/dist-chrome-mv3.zip if-no-files-found: error - - name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD) - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 - with: - 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 - if-no-files-found: error - - name: Upload Firefox artifact uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: @@ -240,11 +242,13 @@ jobs: _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -347,14 +351,16 @@ jobs: crowdin-push: name: Crowdin Push - if: github.ref == 'refs/heads/main' + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 needs: - build - build-safari steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -392,7 +398,10 @@ jobs: - crowdin-push steps: - name: Check if any job failed - if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-browser') + && contains(needs.*.result, 'failure') run: exit 1 - name: Login to Azure - Prod Subscription diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index f88c4767407..ac39ab2608b 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -1,7 +1,8 @@ name: Build CLI on: - pull_request: + pull_request_target: + types: [opened, synchronize] branches-ignore: - 'l10n_master' - 'cf-pages' @@ -34,15 +35,23 @@ defaults: working-directory: apps/cli jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: package_version: ${{ steps.retrieve-package-version.outputs.package_version }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Get Package Version id: retrieve-package-version @@ -58,7 +67,6 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - cli: name: "${{ matrix.os.base }} - ${{ matrix.license_type.readable }}" strategy: @@ -82,8 +90,10 @@ jobs: _WIN_PKG_FETCH_VERSION: 20.11.1 _WIN_PKG_VERSION: 3.5 steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Setup Unix Vars run: | @@ -92,7 +102,7 @@ jobs: awk '{print tolower($0)}')" >> $GITHUB_ENV - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -160,8 +170,10 @@ jobs: _WIN_PKG_FETCH_VERSION: 20.11.1 _WIN_PKG_VERSION: 3.5 steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Setup Windows builder run: | @@ -170,7 +182,7 @@ jobs: choco install nasm --no-progress - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -310,8 +322,10 @@ jobs: env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Print environment run: | @@ -389,7 +403,10 @@ jobs: steps: - name: Check if any job failed working-directory: ${{ github.workspace }} - if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-cli') + && contains(needs.*.result, 'failure') run: exit 1 - name: Login to Azure - Prod Subscription diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 2c89e0d156f..bb2889983b4 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -1,7 +1,8 @@ name: Build Desktop on: - pull_request: + pull_request_target: + types: [opened, synchronize] branches-ignore: - 'l10n_master' - 'cf-pages' @@ -32,12 +33,20 @@ defaults: shell: bash jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + electron-verify: name: Verify Electron Version runs-on: ubuntu-22.04 + needs: + - check-run steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Verify run: | @@ -54,6 +63,8 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: package_version: ${{ steps.retrieve-version.outputs.package_version }} release_channel: ${{ steps.release-channel.outputs.channel }} @@ -65,8 +76,10 @@ jobs: run: working-directory: apps/desktop steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Get Package Version id: retrieve-version @@ -138,11 +151,13 @@ jobs: run: working-directory: apps/desktop steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -168,7 +183,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: cache with: path: | @@ -238,7 +253,8 @@ jobs: windows: name: Windows Build runs-on: windows-2022 - needs: setup + needs: + - setup defaults: run: shell: pwsh @@ -248,11 +264,13 @@ jobs: _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -298,7 +316,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: cache with: path: | @@ -447,7 +465,8 @@ jobs: macos-build: name: MacOS Build runs-on: macos-13 - needs: setup + needs: + - setup env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -456,11 +475,13 @@ jobs: run: working-directory: apps/desktop steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -483,14 +504,14 @@ jobs: - name: Cache Build id: build-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Cache Safari id: safari-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -583,7 +604,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: cache with: path: | @@ -622,11 +643,13 @@ jobs: run: working-directory: apps/desktop steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -649,14 +672,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -749,7 +772,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: cache with: path: | @@ -841,11 +864,13 @@ jobs: run: working-directory: apps/desktop steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -868,14 +893,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -975,7 +1000,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: cache with: path: | @@ -1033,9 +1058,8 @@ jobs: - name: Deploy to TestFlight id: testflight-deploy if: | - (github.ref == 'refs/heads/main' - || github.ref == 'refs/heads/rc' - || github.ref == 'refs/heads/hotfix-rc-desktop') + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') env: APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }} APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP @@ -1050,9 +1074,8 @@ jobs: - name: Post message to a Slack channel id: slack-message if: | - (github.ref == 'refs/heads/main' - || github.ref == 'refs/heads/rc' - || github.ref == 'refs/heads/hotfix-rc-desktop') + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') uses: slackapi/slack-github-action@37ebaef184d7626c5f204ab8d3baff4262dd30f0 # v1.27.0 with: channel-id: C074F5UESQ0 @@ -1088,11 +1111,13 @@ jobs: run: working-directory: apps/desktop steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -1110,14 +1135,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -1139,6 +1164,21 @@ jobs: --file $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ --output none + az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + --name bitwarden_desktop_autofill_app_store_2024.provisionprofile \ + --file $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile \ + --output none + + - name: Set up provisioning profiles + run: | + AUTOFILL_PROFILE_PATH=$HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile + PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles + + mkdir -p "$PROFILES_DIR_PATH" + + AUTOFILL_UUID=$(grep UUID -A1 -a $AUTOFILL_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}") + cp $AUTOFILL_PROFILE_PATH "$PROFILES_DIR_PATH/$AUTOFILL_UUID.provisionprofile" + - name: Get certificates run: | mkdir -p $HOME/certificates @@ -1190,11 +1230,6 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain - - name: Set up provisioning profiles - run: | - cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ - $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile - - name: Increment version shell: pwsh env: @@ -1210,7 +1245,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: cache with: path: | @@ -1279,8 +1314,10 @@ jobs: - macos-package-mas runs-on: ubuntu-22.04 steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -1323,7 +1360,10 @@ jobs: - crowdin-push steps: - name: Check if any job failed - if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') + && contains(needs.*.result, 'failure') run: exit 1 - name: Login to Azure - Prod Subscription diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index ec09f25ac19..ba4f2599f37 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -1,7 +1,8 @@ name: Build Web on: - pull_request: + pull_request_target: + types: [opened, synchronize] branches-ignore: - 'l10n_master' - 'cf-pages' @@ -36,15 +37,23 @@ env: _AZ_REGISTRY: bitwardenprod.azurecr.io jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: version: ${{ steps.version.outputs.value }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Get GitHub sha as version id: version @@ -60,7 +69,8 @@ jobs: build-artifacts: name: Build artifacts runs-on: ubuntu-22.04 - needs: setup + needs: + - setup env: _VERSION: ${{ needs.setup.outputs.version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -89,11 +99,13 @@ jobs: git_metadata: true steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -155,8 +167,10 @@ jobs: env: _VERSION: ${{ needs.setup.outputs.version }} steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Check Branch to Publish env: @@ -249,12 +263,15 @@ jobs: crowdin-push: name: Crowdin Push - if: github.ref == 'refs/heads/main' - needs: build-artifacts + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' + needs: + - build-artifacts runs-on: ubuntu-22.04 steps: - - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -282,9 +299,10 @@ jobs: trigger-web-vault-deploy: name: Trigger web vault deploy - if: github.ref == 'refs/heads/main' + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 - needs: build-artifacts + needs: + - build-artifacts steps: - name: Login to Azure - CI Subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -326,7 +344,10 @@ jobs: - trigger-web-vault-deploy steps: - name: Check if any job failed - if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-web') + && contains(needs.*.result, 'failure') run: exit 1 - name: Login to Azure - Prod Subscription diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index d6f63d48032..0efd9d22f17 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 @@ -37,13 +37,13 @@ jobs: echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: node-version: ${{ steps.retrieve-node-version.outputs.node_version }} - name: Cache NPM id: npm-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: "~/.npm" key: ${{ runner.os }}-npm-chromatic-${{ hashFiles('**/package-lock.json') }} @@ -56,7 +56,7 @@ jobs: run: npm run build-storybook:ci - name: Publish to Chromatic - uses: chromaui/action@bbbf288765438d5fd2be13e1d80d542a39e74108 # v11.12.1 + uses: chromaui/action@dd2eecb9bef44f54774581f4163b0327fd8cf607 # v11.16.3 with: token: ${{ secrets.GITHUB_TOKEN }} projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index dfcd3294b01..540da77b554 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -22,7 +22,7 @@ jobs: crowdin_project_id: "308189" steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index db7fef83fb8..9dc72c7fdda 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Lint filenames (no capital characters) run: | @@ -36,6 +36,7 @@ jobs: ! -path "./.github/*" \ ! -path "*/Cargo.toml" \ ! -path "*/Cargo.lock" \ + ! -path "./apps/desktop/macos/*" \ > tmp.txt diff <(sort .github/whitelist-capital-letters.txt) <(sort tmp.txt) @@ -47,7 +48,7 @@ jobs: echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' diff --git a/.github/workflows/locales-lint.yml b/.github/workflows/locales-lint.yml index ef944526111..8c9447ea50f 100644 --- a/.github/workflows/locales-lint.yml +++ b/.github/workflows/locales-lint.yml @@ -14,9 +14,9 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Checkout base branch repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.base.sha }} path: base diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index c9a4e841ea8..0a561306797 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -91,7 +91,7 @@ jobs: _PKG_VERSION: ${{ needs.setup.outputs.release-version }} steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -128,7 +128,7 @@ jobs: _PKG_VERSION: ${{ needs.setup.outputs.release-version }} steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -168,7 +168,7 @@ jobs: _PKG_VERSION: ${{ needs.setup.outputs.release-version }} steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index c46a7a27601..5ef378ad439 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -183,7 +183,7 @@ jobs: _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} steps: - name: Checkout Repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -227,7 +227,7 @@ jobs: _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} steps: - name: Checkout Repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Print Environment run: | diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml index 7e0e8737344..09f5ddc6318 100644 --- a/.github/workflows/publish-web.yml +++ b/.github/workflows/publish-web.yml @@ -26,7 +26,7 @@ jobs: tag_version: ${{ steps.version.outputs.tag }} steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Branch check if: ${{ inputs.publish_type != 'Dry Run' }} @@ -66,7 +66,7 @@ jobs: echo "Github Release Option: $_RELEASE_OPTION" - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 ########## ACR ########## - name: Login to Azure - PROD Subscription diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index aed9ab293e8..4c3321c015d 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -26,7 +26,7 @@ jobs: release-version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -55,7 +55,7 @@ jobs: needs: setup steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Testing locales - extName length run: | diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 8660744f944..05c53f9752d 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -26,7 +26,7 @@ jobs: release-version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Branch check if: ${{ inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 7518daf0b16..c1646997201 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -23,7 +23,7 @@ jobs: node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Branch check run: | @@ -124,12 +124,12 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -214,12 +214,12 @@ jobs: NODE_OPTIONS: --max_old_space_size=4096 steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -403,12 +403,12 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -426,14 +426,14 @@ jobs: - name: Cache Build id: build-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Cache Safari id: safari-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -537,12 +537,12 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -560,14 +560,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -750,12 +750,12 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -773,14 +773,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -1010,7 +1010,7 @@ jobs: - release steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup git config run: | diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index b0ddc4b804d..d9394347f60 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -26,7 +26,7 @@ jobs: release-channel: ${{ steps.release-channel.outputs.channel }} steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index e3462a98fb6..faa398f6d67 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -23,7 +23,7 @@ jobs: tag_version: ${{ steps.version.outputs.tag }} steps: - name: Checkout repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/version-bump.yml b/.github/workflows/repository-management.yml similarity index 54% rename from .github/workflows/version-bump.yml rename to .github/workflows/repository-management.yml index 7f6dfef79cf..21de47f13ba 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/repository-management.yml @@ -1,124 +1,130 @@ -name: Version Bump +name: Repository management on: workflow_dispatch: inputs: + task: + default: "Version Bump" + description: "Task to execute" + options: + - "Version Bump" + - "Version Bump and Cut rc" + required: true + type: choice bump_browser: - description: "Bump Browser?" + description: "Bump Browser version?" type: boolean default: false bump_cli: - description: "Bump CLI?" + description: "Bump CLI version?" type: boolean default: false bump_desktop: - description: "Bump Desktop?" + description: "Bump Desktop version?" type: boolean default: false bump_web: - description: "Bump Web?" + description: "Bump Web version?" type: boolean default: false + target_ref: + default: "main" + description: "Branch/Tag to target for cut" + required: true + type: string version_number_override: description: "New version override (leave blank for automatic calculation, example: '2024.1.0')" required: false type: string - cut_rc_branch: - description: "Cut RC branch?" - default: true - type: boolean - enable_slack_notification: - description: "Enable Slack notifications for upcoming release?" - default: false - type: boolean + jobs: + setup: + name: Setup + runs-on: ubuntu-24.04 + outputs: + branch: ${{ steps.set-branch.outputs.branch }} + token: ${{ steps.app-token.outputs.token }} + steps: + - name: Set branch + id: set-branch + env: + TASK: ${{ inputs.task }} + run: | + if [[ "$TASK" == "Version Bump" ]]; then + BRANCH="none" + elif [[ "$TASK" == "Version Bump and Cut rc" ]]; then + BRANCH="rc" + fi + + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + + - name: Generate GH App token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + id: app-token + with: + app-id: ${{ secrets.BW_GHAPP_ID }} + private-key: ${{ secrets.BW_GHAPP_KEY }} + + + cut_branch: + name: Cut branch + if: ${{ needs.setup.outputs.branch == 'rc' }} + needs: setup + runs-on: ubuntu-24.04 + steps: + - name: Check out target ref + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ inputs.target_ref }} + token: ${{ needs.setup.outputs.token }} + + - name: Check if ${{ needs.setup.outputs.branch }} branch exists + env: + BRANCH_NAME: ${{ needs.setup.outputs.branch }} + run: | + if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then + echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Cut branch + env: + BRANCH_NAME: ${{ needs.setup.outputs.branch }} + run: | + git switch --quiet --create $BRANCH_NAME + git push --quiet --set-upstream origin $BRANCH_NAME + + bump_version: name: Bump Version - runs-on: ubuntu-22.04 + if: ${{ always() }} + runs-on: ubuntu-24.04 + needs: + - cut_branch + - setup outputs: version_browser: ${{ steps.set-final-version-output.outputs.version_browser }} version_cli: ${{ steps.set-final-version-output.outputs.version_cli }} version_desktop: ${{ steps.set-final-version-output.outputs.version_desktop }} version_web: ${{ steps.set-final-version-output.outputs.version_web }} steps: - - name: Validate version input + - name: Validate version input format if: ${{ inputs.version_number_override != '' }} uses: bitwarden/gh-actions/version-check@main with: version: ${{ inputs.version_number_override }} - - name: Slack Notification Check - run: | - if [[ "${{ inputs.enable_slack_notification }}" == true ]]; then - echo "Slack notifications enabled." - else - echo "Slack notifications disabled." - fi - - - name: Checkout Branch - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out branch + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main + token: ${{ needs.setup.outputs.token }} - - name: Check if RC branch exists - if: ${{ inputs.cut_rc_branch == true }} + - name: Configure Git run: | - remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l) - if [[ "${remote_rc_branch_check}" -gt 0 ]]; then - echo "Remote RC branch exists." - echo "Please delete current RC branch before running again." - exit 1 - fi - - - name: Login to Azure - CI Subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "github-gpg-private-key, - github-gpg-private-key-passphrase" - - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 - with: - gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }} - passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }} - git_user_signingkey: true - git_commit_gpgsign: true - - - name: Setup git - run: | - git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com" - git config --local user.name "bitwarden-devops-bot" - - - name: Create Version Branch - id: create-branch - run: | - CLIENTS=() - if [[ ${{ inputs.bump_browser }} == true ]]; then - CLIENTS+=("browser") - fi - if [[ ${{ inputs.bump_cli }} == true ]]; then - CLIENTS+=("cli") - fi - if [[ ${{ inputs.bump_desktop }} == true ]]; then - CLIENTS+=("desktop") - fi - if [[ ${{ inputs.bump_web }} == true ]]; then - CLIENTS+=("web") - fi - printf -v joined '%s,' "${CLIENTS[@]}" - echo "client=${joined%,}" >> $GITHUB_OUTPUT - - NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d") - git switch -c $NAME - echo "name=$NAME" >> $GITHUB_OUTPUT + git config --local user.email "actions@github.com" + git config --local user.name "Github Actions" ######################## # VERSION BUMP SECTION # @@ -165,7 +171,9 @@ jobs: - name: Bump Browser Version - Version Override if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }} id: bump-browser-version-override - run: npm version --workspace=@bitwarden/browser ${{ inputs.version_number_override }} + env: + VERSION: ${{ inputs.version_number_override }} + run: npm version --workspace=@bitwarden/browser $VERSION - name: Bump Browser Version - Automatic Calculation if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }} @@ -250,7 +258,9 @@ jobs: - name: Bump CLI Version - Version Override if: ${{ inputs.bump_cli == true && inputs.version_number_override != '' }} id: bump-cli-version-override - run: npm version --workspace=@bitwarden/cli ${{ inputs.version_number_override }} + env: + VERSION: ${{ inputs.version_number_override }} + run: npm version --workspace=@bitwarden/cli $VERSION - name: Bump CLI Version - Automatic Calculation if: ${{ inputs.bump_cli == true && inputs.version_number_override == '' }} @@ -300,7 +310,9 @@ jobs: - name: Bump Desktop Version - Root - Version Override if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }} id: bump-desktop-version-override - run: npm version --workspace=@bitwarden/desktop ${{ inputs.version_number_override }} + env: + VERSION: ${{ inputs.version_number_override }} + run: npm version --workspace=@bitwarden/desktop $VERSION - name: Bump Desktop Version - Root - Automatic Calculation if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }} @@ -311,7 +323,9 @@ jobs: - name: Bump Desktop Version - App - Version Override if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }} - run: npm version ${{ inputs.version_number_override }} + env: + VERSION: ${{ inputs.version_number_override }} + run: npm version $VERSION working-directory: "apps/desktop/src" - name: Bump Desktop Version - App - Automatic Calculation @@ -362,7 +376,9 @@ jobs: - name: Bump Web Version - Version Override if: ${{ inputs.bump_web == true && inputs.version_number_override != '' }} id: bump-web-version-override - run: npm version --workspace=@bitwarden/web-vault ${{ inputs.version_number_override }} + env: + VERSION: ${{ inputs.version_number_override }} + run: npm version --workspace=@bitwarden/web-vault $VERSION - name: Bump Web Version - Automatic Calculation if: ${{ inputs.bump_web == true && inputs.version_number_override == '' }} @@ -375,27 +391,29 @@ jobs: - name: Set final version output id: set-final-version-output + env: + VERSION: ${{ inputs.version_number_override }} run: | if [[ "${{ steps.bump-browser-version-override.outcome }}" = "success" ]]; then - echo "version_browser=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT + echo "version_browser=$VERSION" >> $GITHUB_OUTPUT elif [[ "${{ steps.bump-browser-version-automatic.outcome }}" = "success" ]]; then echo "version_browser=${{ steps.calculate-next-browser-version.outputs.version }}" >> $GITHUB_OUTPUT fi if [[ "${{ steps.bump-cli-version-override.outcome }}" = "success" ]]; then - echo "version_cli=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT + echo "version_cli=$VERSION" >> $GITHUB_OUTPUT elif [[ "${{ steps.bump-cli-version-automatic.outcome }}" = "success" ]]; then echo "version_cli=${{ steps.calculate-next-cli-version.outputs.version }}" >> $GITHUB_OUTPUT fi if [[ "${{ steps.bump-desktop-version-override.outcome }}" = "success" ]]; then - echo "version_desktop=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT + echo "version_desktop=$VERSION" >> $GITHUB_OUTPUT elif [[ "${{ steps.bump-desktop-version-automatic.outcome }}" = "success" ]]; then echo "version_desktop=${{ steps.calculate-next-desktop-version.outputs.version }}" >> $GITHUB_OUTPUT fi if [[ "${{ steps.bump-web-version-override.outcome }}" = "success" ]]; then - echo "version_web=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT + echo "version_web=$VERSION" >> $GITHUB_OUTPUT elif [[ "${{ steps.bump-web-version-automatic.outcome }}" = "success" ]]; then echo "version_web=${{ steps.calculate-next-web-version.outputs.version }}" >> $GITHUB_OUTPUT fi @@ -416,199 +434,52 @@ jobs: - name: Push changes if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - env: - PR_BRANCH: ${{ steps.create-branch.outputs.name }} - run: git push -u origin $PR_BRANCH + run: git push - - name: Generate PR message - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - id: pr-message - run: | - MESSAGE="" - if [[ "${{ inputs.bump_browser }}" == "true" ]]; then - MESSAGE+=$' Browser version bump to ${{ steps.set-final-version-output.outputs.version_browser }}\n' - fi - if [[ "${{ inputs.bump_cli }}" == "true" ]]; then - MESSAGE+=$' CLI version bump to ${{ steps.set-final-version-output.outputs.version_cli }}\n' - fi - - if [[ "${{ inputs.bump_desktop }}" == "true" ]]; then - MESSAGE+=$' Desktop version bump to ${{ steps.set-final-version-output.outputs.version_desktop }}\n' - fi - - if [[ "${{ inputs.bump_web }}" == "true" ]]; then - MESSAGE+=$' Web version bump to ${{ steps.set-final-version-output.outputs.version_web }}\n' - fi - - echo "MESSAGE<> $GITHUB_ENV - echo "$MESSAGE" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - - - name: Generate GH App token - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 - id: app-token - with: - app-id: ${{ secrets.BW_GHAPP_ID }} - private-key: ${{ secrets.BW_GHAPP_KEY }} - owner: ${{ github.repository_owner }} - - - name: Create Version PR - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - id: create-pr - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - PR_BRANCH: ${{ steps.create-branch.outputs.name }} - TITLE: "Bump client(s) version" - run: | - PR_URL=$(gh pr create --title "$TITLE" \ - --base "main" \ - --head "$PR_BRANCH" \ - --label "version update" \ - --label "automated pr" \ - --body " - ## Type of change - - [ ] Bug fix - - [ ] New feature development - - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc) - - [ ] Build/deploy pipeline (DevOps) - - [X] Other - - ## Objective - $MESSAGE") - - echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT - - - name: Approve PR - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }} - run: gh pr review $PR_NUMBER --approve - - - name: Merge PR - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }} - run: gh pr merge $PR_NUMBER --squash --auto --delete-branch - - - name: Report upcoming browser release version to Slack - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_browser != '' && inputs.enable_slack_notification == true }} - uses: bitwarden/gh-actions/report-upcoming-release-version@main - with: - version: ${{ steps.set-final-version-output.outputs.version_browser }} - project: browser - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Report upcoming cli release version to Slack - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_cli != '' && inputs.enable_slack_notification == true }} - uses: bitwarden/gh-actions/report-upcoming-release-version@main - with: - version: ${{ steps.set-final-version-output.outputs.version_cli }} - project: cli - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Report upcoming desktop release version to Slack - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_desktop != '' && inputs.enable_slack_notification == true }} - uses: bitwarden/gh-actions/report-upcoming-release-version@main - with: - version: ${{ steps.set-final-version-output.outputs.version_desktop }} - project: desktop - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Report upcoming web release version to Slack - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_web != '' && inputs.enable_slack_notification == true }} - uses: bitwarden/gh-actions/report-upcoming-release-version@main - with: - version: ${{ steps.set-final-version-output.outputs.version_web }} - project: web - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - cut_rc: - name: Cut RC branch - if: ${{ inputs.cut_rc_branch == true }} - needs: bump_version - runs-on: ubuntu-22.04 + cherry_pick: + name: Cherry-Pick Commit(s) + if: ${{ needs.setup.outputs.branch == 'rc' }} + runs-on: ubuntu-24.04 + needs: + - bump_version + - setup steps: - - name: Checkout Branch - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Check out main branch + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main + token: ${{ needs.setup.outputs.token }} - ### Browser - - name: Browser - Verify version has been updated - if: ${{ inputs.bump_browser == true }} - env: - NEW_VERSION: ${{ needs.bump_version.outputs.version_browser }} + - name: Configure Git run: | - # Wait for version to change. - while : ; do - echo "Waiting for version to be updated..." - git pull --force - CURRENT_VERSION=$(cat package.json | jq -r '.version') + git config --local user.email "actions@github.com" + git config --local user.name "Github Actions" - # If the versions don't match we continue the loop, otherwise we break out of the loop. - [[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break - sleep 10 - done - working-directory: apps/browser - - ### CLI - - name: CLI - Verify version has been updated - if: ${{ inputs.bump_cli == true }} - env: - NEW_VERSION: ${{ needs.bump_version.outputs.version_cli }} + - name: Perform cherry-pick(s) run: | - # Wait for version to change. - while : ; do - echo "Waiting for version to be updated..." - git pull --force - CURRENT_VERSION=$(cat package.json | jq -r '.version') + # Function for cherry-picking + cherry_pick () { + local package_path="apps/$1/package.json" + local source_branch=$2 + local destination_branch=$3 - # If the versions don't match we continue the loop, otherwise we break out of the loop. - [[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break - sleep 10 - done - working-directory: apps/cli + # Get project commit/version from source branch + git switch $source_branch + SOURCE_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 $package_path) + SOURCE_VERSION=$(cat $package_path | jq -r '.version') - ### Desktop - - name: Desktop - Verify version has been updated - if: ${{ inputs.bump_desktop == true }} - env: - NEW_VERSION: ${{ needs.bump_version.outputs.version_desktop }} - run: | - # Wait for version to change. - while : ; do - echo "Waiting for version to be updated..." - git pull --force - CURRENT_VERSION=$(cat package.json | jq -r '.version') + # Get project commit/version from destination branch + git switch $destination_branch + DESTINATION_VERSION=$(cat $package_path | jq -r '.version') - # If the versions don't match we continue the loop, otherwise we break out of the loop. - [[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break - sleep 10 - done - working-directory: apps/desktop + if [[ "$DESTINATION_VERSION" != "$SOURCE_VERSION" ]]; then + git cherry-pick --strategy-option=theirs -x $SOURCE_COMMIT + git push -u origin $destination_branch + fi - ### Web - - name: Web - Verify version has been updated - if: ${{ inputs.bump_web == true }} - env: - NEW_VERSION: ${{ needs.bump_version.outputs.version_web }} - run: | - # Wait for version to change. - while : ; do - echo "Waiting for version to be updated..." - git pull --force - CURRENT_VERSION=$(cat package.json | jq -r '.version') - - # If the versions don't match we continue the loop, otherwise we break out of the loop. - [[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break - sleep 10 - done - working-directory: apps/web - - - name: Cut RC branch - run: | - git switch --quiet --create rc - git push --quiet --set-upstream origin rc + # Cherry-pick from 'main' into 'rc' + cherry_pick browser main rc + cherry_pick cli main rc + cherry_pick desktop main rc + cherry_pick web main rc diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 143d049bd63..bf17459c21c 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} @@ -46,7 +46,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 + uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 with: sarif_file: cx_result.sarif @@ -60,7 +60,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ea08a24373..0c324cb8748 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Get Node Version id: retrieve-node-version @@ -50,7 +50,7 @@ jobs: echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -120,7 +120,7 @@ jobs: sudo apt-get install -y gnome-keyring dbus-x11 - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Build working-directory: ./apps/desktop/desktop_native diff --git a/.github/workflows/version-auto-bump.yml b/.github/workflows/version-auto-bump.yml index cc6feeba026..f41261cb39a 100644 --- a/.github/workflows/version-auto-bump.yml +++ b/.github/workflows/version-auto-bump.yml @@ -8,27 +8,55 @@ on: jobs: bump-version: name: Bump Desktop Version - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - name: Login to Azure - CI Subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Generate GH App token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + id: app-token with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + app-id: ${{ secrets.BW_GHAPP_ID }} + private-key: ${{ secrets.BW_GHAPP_KEY }} - - name: Retrieve bot secrets - id: retrieve-bot-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main + - name: Check out target ref + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - keyvault: bitwarden-ci - secrets: "github-pat-bitwarden-devops-bot-repo-scope" + ref: main + token: ${{ steps.app-token.outputs.token }} - - name: Trigger Version Bump workflow - env: - GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} + - name: Configure Git run: | - echo '{"cut_rc_branch": "false", - "bump_browser": "false", - "bump_cli": "false", - "bump_desktop": "true", - "bump_web": "false"}' | \ - gh workflow run version-bump.yml --json --repo bitwarden/clients + git config --local user.email "actions@github.com" + git config --local user.name "Github Actions" + + - name: Get current Desktop version + id: current-desktop-version + run: | + CURRENT_VERSION=$(cat package.json | jq -r '.version') + echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + working-directory: apps/desktop + + - name: Calculate next Desktop release version + id: calculate-next-desktop-version + uses: bitwarden/gh-actions/version-next@main + with: + version: ${{ steps.current-desktop-version.outputs.version }} + + - name: Bump Desktop Version - Root - Automatic Calculation + id: bump-desktop-version-automatic + env: + VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }} + run: npm version --workspace=@bitwarden/desktop $VERSION + + - name: Bump Desktop Version - App - Automatic Calculation + env: + VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }} + run: npm version $VERSION + working-directory: "apps/desktop/src" + + - name: Commit files + env: + VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }} + run: git commit -m "Bumped Desktop client to $VERSION" -a + + - name: Push changes + run: git push diff --git a/angular.json b/angular.json index 1670491b6fd..7053050262e 100644 --- a/angular.json +++ b/angular.json @@ -128,10 +128,10 @@ "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { - "browserTarget": "test-storybook:build:production" + "buildTarget": "test-storybook:build:production" }, "development": { - "browserTarget": "test-storybook:build:development" + "buildTarget": "test-storybook:build:development" } }, "defaultConfiguration": "development" diff --git a/apps/browser/gulpfile.js b/apps/browser/gulpfile.js index 573f86efc18..ed977df4715 100644 --- a/apps/browser/gulpfile.js +++ b/apps/browser/gulpfile.js @@ -9,7 +9,6 @@ const replace = require("gulp-replace"); const manifest = require("./src/manifest.json"); const manifestVersion = parseInt(process.env.MANIFEST_VERSION || manifest.version); -const betaBuild = process.env.BETA_BUILD === "1"; const paths = { build: "./build/", @@ -17,27 +16,11 @@ const paths = { safari: "./src/safari/", }; -/** - * Converts a number to a tuple containing two Uint16's - * @param num {number} This number is expected to be a integer style number with no decimals - * - * @returns {number[]} A tuple containing two elements that are both numbers. - */ -function numToUint16s(num) { - var arr = new ArrayBuffer(4); - var view = new DataView(arr); - view.setUint32(0, num, false); - return [view.getUint16(0), view.getUint16(2)]; -} - function buildString() { var build = ""; if (process.env.MANIFEST_VERSION) { build = `-mv${process.env.MANIFEST_VERSION}`; } - if (betaBuild) { - build += "-beta"; - } if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") { build = `-${process.env.BUILD_NUMBER}`; } @@ -71,9 +54,6 @@ function distFirefox() { manifest.optional_permissions = manifest.optional_permissions.filter( (permission) => permission !== "privacy", ); - if (betaBuild) { - manifest = applyBetaLabels(manifest); - } return manifest; }); } @@ -90,9 +70,6 @@ function distOpera() { delete manifest.commands._execute_sidebar_action; } - if (betaBuild) { - manifest = applyBetaLabels(manifest); - } return manifest; }); } @@ -102,9 +79,6 @@ function distChrome() { delete manifest.applications; delete manifest.sidebar_action; delete manifest.commands._execute_sidebar_action; - if (betaBuild) { - manifest = applyBetaLabels(manifest); - } return manifest; }); } @@ -114,9 +88,6 @@ function distEdge() { delete manifest.applications; delete manifest.sidebar_action; delete manifest.commands._execute_sidebar_action; - if (betaBuild) { - manifest = applyBetaLabels(manifest); - } return manifest; }); } @@ -237,9 +208,6 @@ async function safariCopyBuild(source, dest) { delete manifest.commands._execute_sidebar_action; delete manifest.optional_permissions; manifest.permissions.push("nativeMessaging"); - if (betaBuild) { - manifest = applyBetaLabels(manifest); - } return manifest; }), ), @@ -254,30 +222,6 @@ function stdOutProc(proc) { proc.stderr.on("data", (data) => console.error(data.toString())); } -function applyBetaLabels(manifest) { - manifest.name = "Bitwarden Password Manager BETA"; - manifest.short_name = "Bitwarden BETA"; - manifest.description = "THIS EXTENSION IS FOR BETA TESTING BITWARDEN."; - if (process.env.GITHUB_RUN_ID) { - const existingVersionParts = manifest.version.split("."); // 3 parts expected 2024.4.0 - - // GITHUB_RUN_ID is a number like: 8853654662 - // which will convert to [ 4024, 3206 ] - // and a single incremented id of 8853654663 will become [ 4024, 3207 ] - const runIdParts = numToUint16s(parseInt(process.env.GITHUB_RUN_ID)); - - // Only use the first 2 parts from the given version number and base the other 2 numbers from the GITHUB_RUN_ID - // Example: 2024.4.4024.3206 - const betaVersion = `${existingVersionParts[0]}.${existingVersionParts[1]}.${runIdParts[0]}.${runIdParts[1]}`; - - manifest.version_name = `${betaVersion} beta - ${process.env.GITHUB_SHA.slice(0, 8)}`; - manifest.version = betaVersion; - } else { - manifest.version = `${manifest.version}.0`; - } - return manifest; -} - exports["dist:firefox"] = distFirefox; exports["dist:chrome"] = distChrome; exports["dist:opera"] = distOpera; diff --git a/apps/browser/package.json b/apps/browser/package.json index 4ff4615983b..f79d646d851 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.10.999", + "version": "2024.11.1", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", @@ -10,12 +10,9 @@ "build:watch:safari": "cross-env MANIFEST_VERSION=3 BROWSER=safari webpack --watch", "build:watch:mv2": "webpack --watch", "build:prod": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" webpack", - "build:prod:beta": "cross-env BETA_BUILD=1 NODE_ENV=production webpack", "build:prod:watch": "cross-env NODE_ENV=production webpack --watch", "dist": "npm run build:prod && gulp dist", - "dist:beta": "npm run build:prod:beta && cross-env BETA_BUILD=1 gulp dist", "dist:mv3": "cross-env MANIFEST_VERSION=3 npm run build:prod && cross-env MANIFEST_VERSION=3 gulp dist", - "dist:mv3:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist", "dist:chrome": "npm run build:prod && gulp dist:chrome", "dist:chrome:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist:chrome", "dist:firefox": "npm run build:prod && gulp dist:firefox", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 415ade9820b..b2f849964fa 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "نوع اسم المستخدم" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "جارٍ تسجيل الدخول على" - }, "opensInANewWindow": { "message": "تُفتح في نافذة جديدة" }, diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index c041c6c1923..94b1d9ea4c7 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "E-poçt yarat" }, + "generatorBoundariesHint": { + "message": "Dəyər $MIN$-$MAX$ arasında olmalıdır", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "İstifadəçi adı növü" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Giriş edilir" - }, "opensInANewWindow": { "message": "Yeni bir pəncərədə açılır" }, diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index e6488e5ff83..294ffea0563 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Тып імя карыстальніка" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Увайсці на" - }, "opensInANewWindow": { "message": "Адкрываць у новым акне" }, diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 626221f06d1..e8249decb6d 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Генериране на електронна поща" }, + "generatorBoundariesHint": { + "message": "Стойността трябва да бъде между $MIN$ и $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Тип потребителско име" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Вписване в" - }, "opensInANewWindow": { "message": "Отваря се в нов прозорец" }, diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index b808ab5eb62..c05fd642e79 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 2793b4dee62..bc8d843045e 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index eee9e4956ce..d3cdc23d8c5 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Tipus de nom d'usuari" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Inici de sessió en" - }, "opensInANewWindow": { "message": "S'obri en una finestra nova" }, diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 80cce02f219..9943cbebc12 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Vygenerovat e-mail" }, + "generatorBoundariesHint": { + "message": "Hodnota musí být mezi $MIN$ a $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Typ uživatelského jména" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Přihlašování na" - }, "opensInANewWindow": { "message": "Otevře se v novém okně" }, diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 13a75b413f7..5bec66dec95 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Math o enw defnyddiwr" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Mewngofnodi ar" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 437a3fa7df4..60949c317aa 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generér e-mail" }, + "generatorBoundariesHint": { + "message": "Værdi skal være mellem $MIN$ og $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Brugernavnstype" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logger ind på" - }, "opensInANewWindow": { "message": "Åbnes i et nyt vindue" }, diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 1e3a38ffc7b..df664cf8d36 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "E-Mail generieren" }, + "generatorBoundariesHint": { + "message": "Wert muss zwischen $MIN$ und $MAX$ liegen", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Benutzernamenstyp" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Anmelden bei" - }, "opensInANewWindow": { "message": "Wird in einem neuen Fenster geöffnet" }, diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index c2bc58519cc..bd2f76249bf 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -1022,7 +1022,7 @@ "message": "Ρώτησε για να ενημερώσεις τον κωδικό πρόσβασης μιας σύνδεσης όταν εντοπιστεί μια αλλαγή σε έναν ιστότοπο. Ισχύει για όλους τους συνδεδεμένους λογαριασμούς." }, "enableUsePasskeys": { - "message": "Ρώτησε για αποθήκευση και χρήση κλειδιών πρόσβασης" + "message": "Ερώτηση για αποθήκευση και χρήση κλειδιών πρόσβασης" }, "usePasskeysDesc": { "message": "Ρώτησε με για την αποθήκευση νέων συνθηματικών ή σύνδεση με κλειδιά πρόσβασης αποθηκευμένα στο θησαυ/κιό μου. Ισχύει για όλους τους συνδεδεμένους λογαριασμούς." @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Δημιουργία email" }, + "generatorBoundariesHint": { + "message": "Η τιμή πρέπει να είναι μεταξύ $MIN$ και $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Τύπος ονόματος χρήστη" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Σύνδεση στο" - }, "opensInANewWindow": { "message": "Ανοίγει σε νέο παράθυρο" }, @@ -4753,7 +4764,7 @@ "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "Κόμμα", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 2ce31f933bc..3832fc5b9e3 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -152,6 +152,15 @@ "copyLicenseNumber": { "message": "Copy license number" }, + "copyPrivateKey": { + "message": "Copy private key" + }, + "copyPublicKey": { + "message": "Copy public key" + }, + "copyFingerprint": { + "message": "Copy fingerprint" + }, "copyCustomField": { "message": "Copy $FIELD$", "placeholders": { @@ -1764,6 +1773,9 @@ "typeIdentity": { "message": "Identity" }, + "typeSshKey": { + "message": "SSH key" + }, "newItemHeader": { "message": "New $TYPE$", "placeholders": { @@ -2877,6 +2889,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3246,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, @@ -4582,6 +4605,30 @@ "enterprisePolicyRequirementsApplied": { "message": "Enterprise policy requirements have been applied to this setting" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, + "sshKeyAlgorithm": { + "message": "Key type" + }, + "sshKeyAlgorithmED25519": { + "message": "ED25519" + }, + "sshKeyAlgorithmRSA2048": { + "message": "RSA 2048-Bit" + }, + "sshKeyAlgorithmRSA3072": { + "message": "RSA 3072-Bit" + }, + "sshKeyAlgorithmRSA4096": { + "message": "RSA 4096-Bit" + }, "retry": { "message": "Retry" }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 71161b14e5c..87eee8c90c0 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 4265ffa6964..c5b294aa3a2 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username Type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 5c7e82f89ba..42e56a56ba4 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -20,16 +20,16 @@ "message": "Crear cuenta" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "¿Nuevo en Bitwarden?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Iniciar sesión con clave de acceso" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Usar inicio de sesión único" }, "welcomeBack": { - "message": "Welcome back" + "message": "Bienvenido de nuevo" }, "setAStrongPassword": { "message": "Establece una contraseña fuerte" @@ -84,7 +84,7 @@ "message": "Incorporarse a la organización" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "Unirse a $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -120,7 +120,7 @@ "message": "Copiar contraseña" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Copiar frase de contraseña" }, "copyNote": { "message": "Copiar nota" @@ -153,7 +153,7 @@ "message": "Copiar número de licencia" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Copiar $FIELD$", "placeholders": { "field": { "content": "$1", @@ -162,13 +162,13 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Copiar sitio web" }, "copyNotes": { - "message": "Copy notes" + "message": "Copiar notas" }, "fill": { - "message": "Fill", + "message": "Rellenar", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -223,13 +223,13 @@ "message": "Añadir elemento" }, "accountEmail": { - "message": "Account email" + "message": "Correo electrónico de la cuenta" }, "requestHint": { - "message": "Request hint" + "message": "Solicitar pista" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Solicitar pista de la contraseña" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { "message": "Enter your account email address and your password hint will be sent to you" @@ -427,7 +427,7 @@ "message": "Generar contraseña" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Generar frase de contraseña" }, "regeneratePassword": { "message": "Regenerar contraseña" @@ -567,7 +567,7 @@ "message": "Notas" }, "privateNote": { - "message": "Private note" + "message": "Nota privada" }, "note": { "message": "Nota" @@ -624,7 +624,7 @@ "message": "Tiempo de sesión agotado" }, "vaultTimeoutHeader": { - "message": "Vault timeout" + "message": "Tiempo de espera de la caja fuerte" }, "otherOptions": { "message": "Otras opciones" @@ -645,13 +645,13 @@ "message": "Tu caja fuerte está bloqueada. Verifica tu identidad para continuar." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Tu caja fuerte está bloqueada" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "Tu cuenta está bloqueada" }, "or": { - "message": "or" + "message": "o" }, "unlock": { "message": "Desbloquear" @@ -676,7 +676,7 @@ "message": "Tiempo de espera de la caja fuerte" }, "vaultTimeout1": { - "message": "Timeout" + "message": "Tiempo de espera" }, "lockNow": { "message": "Bloquear" @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Tipo de nombre de usuario" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Iniciando sesión en" - }, "opensInANewWindow": { "message": "Abre en una nueva ventana" }, @@ -4697,11 +4708,11 @@ "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "Más", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "Igual", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { @@ -4725,15 +4736,15 @@ "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Contrabarra", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "Dos puntos", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Punto y coma", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 38543a88842..81538f3bb22 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Kasutajanime tüüp" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Sisselogimas kui" - }, "opensInANewWindow": { "message": "Avaneb uues aknas" }, diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 3bdc49f61ad..3d0ac87989a 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Erabiltzaile izen mota" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Leiho berri batean irekitzen da" }, diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 94c420155e7..15aa92a669d 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "نوع نام کاربری" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "ورود با" - }, "opensInANewWindow": { "message": "در پنجره جدید باز می‌شود" }, diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 3d0fe6ea64d..054362a4a26 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -20,16 +20,16 @@ "message": "Luo tili" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Oletko uusi Bitwarden-käyttäjä?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Kirjaudu pääsyavaimella" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Käytä kertakirjautumista" }, "welcomeBack": { - "message": "Welcome back" + "message": "Tervetuloa takaisin" }, "setAStrongPassword": { "message": "Aseta vahva salasana" @@ -120,7 +120,7 @@ "message": "Kopioi salasana" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Kopioi salalause" }, "copyNote": { "message": "Kopioi merkinnät" @@ -168,7 +168,7 @@ "message": "Kopioi merkinnät" }, "fill": { - "message": "Fill", + "message": "Täytä", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -427,7 +427,7 @@ "message": "Luo salasana" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Luo salalause" }, "regeneratePassword": { "message": "Luo uusi salasana" @@ -591,7 +591,7 @@ "message": "Avaa verkkosivusto" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "Avaa verkkosivusto $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -846,7 +846,7 @@ "message": "Kirjaudu" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Kirjaudu Bitwardeniin" }, "restartRegistration": { "message": "Aloita rekisteröityminen alusta" @@ -1424,7 +1424,7 @@ "message": "Palvelimen URL" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "Itse ylläpidetyn palvelimen URL-osoite", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1795,13 +1795,13 @@ "message": "Salasanahistoria" }, "generatorHistory": { - "message": "Generator history" + "message": "Generaattorihistoria" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Tyhjennä generaattorihistoria" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Jos jatkat, kaikki generaattorihistorian kohteet poistetaan. Haluatko varmasti jatkaa?" }, "back": { "message": "Takaisin" @@ -1920,10 +1920,10 @@ "message": "Tyhjennä historia" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Mitään näytettävää ei ole" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Et ole luonut mitään hiljattain" }, "remove": { "message": "Poista" @@ -2710,7 +2710,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "/$TOTAL$", "placeholders": { "total": { "content": "$1", @@ -2875,7 +2875,21 @@ "message": "Luo käyttäjätunnus" }, "generateEmail": { - "message": "Generate email" + "message": "Luo sähköpostiosoite" + }, + "generatorBoundariesHint": { + "message": "Arvon tulee olla väliltä $MIN$—$MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } }, "usernameType": { "message": "Käyttäjätunnuksen tyyppi" @@ -2918,11 +2932,11 @@ "message": "Luo sähköpostialias ulkoisella ohjauspalvelulla." }, "forwarderDomainName": { - "message": "Email domain", + "message": "Sähköpostin verkkotunnus", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "Valitse verkkotunnus, jota valittu palvelu tukee", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Kirjaudutaan sijaintiin" - }, "opensInANewWindow": { "message": "Avautuu uudessa ikkunassa" }, @@ -4625,47 +4636,47 @@ "message": "Todennetaan" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "Täytä luotu salasana", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "Salasana luotiin uudelleen", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "Tallennetaanko kirjautumistieto Bitwardeniin?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "Välilyönti", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "Aaltoviiva", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "Graaviaksentti", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "Huutomerkkli", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "Ät-merkki", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { - "message": "Hash sign", + "message": "Ristikkomerkki", "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "Dollarin merkki", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "Prosenttimerkki", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { @@ -4673,27 +4684,27 @@ "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "Ampersandi", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "Asteriski", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "Vasen kaarisulje", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "Oikea kaarisulje", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "Alaviiva", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "Yhdysmerkki", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { @@ -4701,80 +4712,80 @@ "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "Yhtäsuuri kuin", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "Vasen aaltosulje", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "Oikea aaltosulje", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "Vasen hakasulje", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "Oikea hakasulje", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { - "message": "Pipe", + "message": "Putkimerkki", "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Kenoviiva", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "Kaksoispiste", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Puolipiste", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "Lainausmerkki", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "Heittomerkki", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Pienempi kuin", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "Suurempi kuin", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "Pilkku", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "Piste", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Kysymysmerkki", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "Kauttaviiva", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "Pienet kirjaimet" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "Isot kirjaimet" }, "generatedPassword": { - "message": "Generated password" + "message": "Luotu salasana" } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 5adeda013f9..f779ead0dae 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Uri ng username" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index ed2cfb19df7..6aa0ad6d859 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Générer un courriel" }, + "generatorBoundariesHint": { + "message": "La valeur doit être comprise entre $MIN$ et $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Type de nom d'utilisateur" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Connexion sur" - }, "opensInANewWindow": { "message": "S'ouvre dans une nouvelle fenêtre" }, diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index b6aea83973b..498dc34f1af 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index ceaa7370256..0158ef20d67 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "סוג שם משתמש" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index f9b11310f6a..24a4833e569 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index aef566431bb..4f206edd301 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Tip korisničkog imena" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Prijava na" - }, "opensInANewWindow": { "message": "Otvara u novom prozoru" }, diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 32b577b0e83..add9cf6ab89 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Email generálása" }, + "generatorBoundariesHint": { + "message": "Az érték legyen $MIN$ és $MAX$ között.", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Felhasználónév típusa" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Bejelentkezés:" - }, "opensInANewWindow": { "message": "Megnyitás új ablakban" }, diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 1b0f9d19295..87a50d571b1 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Jenis nama pengguna" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 300d99cbed2..0023b9755a5 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -11,7 +11,7 @@ "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { - "message": "Accedi o crea un nuovo account per accedere alla tua cassaforte." + "message": "Entra o crea un nuovo account per accedere alla tua cassaforte." }, "inviteAccepted": { "message": "Invito accettato" @@ -20,16 +20,16 @@ "message": "Crea account" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nuovo a Bitwarden?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Accedi con passkey" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Usa il Single Sign-On" }, "welcomeBack": { - "message": "Welcome back" + "message": "Bentornato" }, "setAStrongPassword": { "message": "Imposta una password robusta" @@ -84,7 +84,7 @@ "message": "Unisciti all'organizzazione" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "Unisciti a $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -120,7 +120,7 @@ "message": "Copia password" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Copia passphrase" }, "copyNote": { "message": "Copia nota" @@ -153,7 +153,7 @@ "message": "Copia numero licenza" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Copia $FIELD$", "placeholders": { "field": { "content": "$1", @@ -162,13 +162,13 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Copia sito web" }, "copyNotes": { - "message": "Copy notes" + "message": "Copia note" }, "fill": { - "message": "Fill", + "message": "Riempi", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -226,13 +226,13 @@ "message": "Account email" }, "requestHint": { - "message": "Request hint" + "message": "Richiedi suggerimento" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Richiedi il suggerimento per la password" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "Inserisci l'indirizzo email dell'account e ti invieremo il tuo suggerimento per la password" }, "passwordHint": { "message": "Suggerimento per la password" @@ -1684,7 +1684,7 @@ "message": "Sig.ra" }, "dr": { - "message": "Dott" + "message": "Dr" }, "mx": { "message": "Mx" @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Tipo di nome utente" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Accedendo su" - }, "opensInANewWindow": { "message": "Si apre in una nuova finestra" }, @@ -4775,6 +4786,6 @@ "message": "Uppercase" }, "generatedPassword": { - "message": "Generated password" + "message": "Password generata" } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 75378889d1a..f0baa16f9f3 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "ユーザー名の種類" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "ログイン先" - }, "opensInANewWindow": { "message": "新しいウィンドウで開く" }, diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index a5f82794ec8..f57b4a50e48 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 3414760d2a3..b8075804229 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 96078aa7edb..a479b47fa46 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index de015d285a4..ed1f9ded490 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "아이디 유형" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 716c474b53e..07d5b3bf8c3 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Vartotojo prisijungimo vardo tipas" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 8e277cf7f8a..870c1e7bef7 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Izveidot e-pastu" }, + "generatorBoundariesHint": { + "message": "Vērtībai jābūt starp $MIN$ un $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Lietotājvārda veids" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Piesakās" - }, "opensInANewWindow": { "message": "Atver jaunā logā" }, diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 2cfabae5b96..0dcc887200b 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index d8e86c4c9e8..150d3197ac6 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 3414760d2a3..b8075804229 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index ecd0b1e5c10..9b647c650bf 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -3,7 +3,7 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden passordbehandler", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { @@ -29,7 +29,7 @@ "message": "Use single sign-on" }, "welcomeBack": { - "message": "Welcome back" + "message": "Velkommen tilbake" }, "setAStrongPassword": { "message": "Set a strong password" @@ -168,7 +168,7 @@ "message": "Copy notes" }, "fill": { - "message": "Fill", + "message": "Fyll", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -357,10 +357,10 @@ "message": "Rediger mappen" }, "newFolder": { - "message": "New folder" + "message": "Ny mappe" }, "folderName": { - "message": "Folder name" + "message": "Mappenavn" }, "folderHintText": { "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" @@ -458,7 +458,7 @@ "description": "deprecated. Use specialCharactersLabel instead." }, "include": { - "message": "Include", + "message": "Inkluder", "description": "Card header for password generator include block" }, "uppercaseDescription": { @@ -466,7 +466,7 @@ "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { - "message": "A-Z", + "message": "A-Å", "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { @@ -478,7 +478,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "Inkluder tall", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -627,7 +627,7 @@ "message": "Vault timeout" }, "otherOptions": { - "message": "Other options" + "message": "Andre valg" }, "rateExtension": { "message": "Gi denne utvidelsen en vurdering" @@ -676,7 +676,7 @@ "message": "Tidsavbrudd i hvelvet" }, "vaultTimeout1": { - "message": "Timeout" + "message": "Tidsavbrudd" }, "lockNow": { "message": "Lås nå" @@ -730,10 +730,10 @@ "message": "Sikkerhet" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "Bekreft hovedpassord" }, "masterPassword": { - "message": "Master password" + "message": "Hovedpassord" }, "masterPassImportant": { "message": "Your master password cannot be recovered if you forget it!" @@ -843,7 +843,7 @@ "message": "Din innloggingsøkt har utløpt." }, "logIn": { - "message": "Log in" + "message": "Logg inn" }, "logInToBitwarden": { "message": "Log in to Bitwarden" @@ -928,7 +928,7 @@ "message": "Ny URI" }, "addDomain": { - "message": "Add domain", + "message": "Legg til domene", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { @@ -1092,7 +1092,7 @@ "message": "This file export will be password protected and require the file password to decrypt." }, "filePassword": { - "message": "File password" + "message": "Filpassord" }, "exportPasswordDescription": { "message": "This password will be used to export and import this file" @@ -1917,10 +1917,10 @@ "message": "Det er ingen passord å liste opp." }, "clearHistory": { - "message": "Clear history" + "message": "Tøm historikk" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Ingenting å vise" }, "nothingGeneratedRecently": { "message": "You haven't generated anything recently" @@ -1984,10 +1984,10 @@ "message": "Lås opp med PIN-kode" }, "setYourPinTitle": { - "message": "Set PIN" + "message": "Velg PIN" }, "setYourPinButton": { - "message": "Set PIN" + "message": "Velg PIN" }, "setYourPinCode": { "message": "Angi PIN-koden din for å låse opp Bitwarden. PIN-innstillingene tilbakestilles hvis du logger deg helt ut av programmet." @@ -2041,7 +2041,7 @@ "message": "Username generator" }, "useThisPassword": { - "message": "Use this password" + "message": "Bruk dette passordet" }, "useThisUsername": { "message": "Use this username" @@ -2186,7 +2186,7 @@ "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Avslutt abonnement" }, "atAnyTime": { "message": "at any time." @@ -2195,7 +2195,7 @@ "message": "By continuing, you agree to the" }, "and": { - "message": "and" + "message": "og" }, "acceptPolicies": { "message": "Ved å merke av denne boksen sier du deg enig i følgende:" @@ -2315,7 +2315,7 @@ "message": "An organization policy has blocked importing items into your individual vault." }, "domainsTitle": { - "message": "Domains", + "message": "Domener", "description": "A category title describing the concept of web domains" }, "excludedDomains": { @@ -2411,7 +2411,7 @@ "message": "Passord beskyttet" }, "copyLink": { - "message": "Copy link" + "message": "Kopier lenke" }, "copySendLink": { "message": "Kopier Send-lenke", @@ -2668,7 +2668,7 @@ "message": "E-postbekreftelse kreves" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "E-post bekreftet" }, "emailVerificationRequiredDesc": { "message": "Du må bekrefte e-posten din for å bruke denne funksjonen. Du kan bekrefte e-postadressen din i netthvelvet." @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Brukernavntype" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logger inn på" - }, "opensInANewWindow": { "message": "Åpnes i et nytt vindu" }, @@ -3236,10 +3247,10 @@ "message": "Uncheck if using a public device" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Godkjenn fra en av dine andre enheter" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Be om administratorgodkjennelse" }, "approveWithMasterPassword": { "message": "Godkjenn med hovedpassord" @@ -3263,7 +3274,7 @@ "message": "No email?" }, "goBack": { - "message": "Go back" + "message": "Gå tilbake" }, "toEditYourEmailAddress": { "message": "to edit your email address." @@ -3279,7 +3290,7 @@ "message": "Generelt" }, "display": { - "message": "Display" + "message": "Vis" }, "accountSuccessfullyCreated": { "message": "Account successfully created!" @@ -3405,7 +3416,7 @@ "message": "— Skriv for å filtrere —" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Innhenter alternativer..." }, "multiSelectNotFound": { "message": "Ingen gjenstander funnet" @@ -3531,7 +3542,7 @@ "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { - "message": "New login", + "message": "Ny innlogging", "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { @@ -3547,7 +3558,7 @@ "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { - "message": "New identity", + "message": "Ny identitet", "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { @@ -3581,7 +3592,7 @@ "message": "Beskrivelse" }, "importSuccess": { - "message": "Data successfully imported" + "message": "Dataene ble vellykket importert" }, "importSuccessNumberOfItems": { "message": "$AMOUNT$ gjenstander totalt ble importert.", @@ -3671,7 +3682,7 @@ "message": "Invalid file password, please use the password you entered when you created the export file." }, "destination": { - "message": "Destination" + "message": "Destinasjon" }, "learnAboutImportOptions": { "message": "Lær mer om importalternativene dine" @@ -3708,7 +3719,7 @@ "message": "Ingen fil er valgt" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "eller kopier/lim inn importfilens innhold" }, "instructionsFor": { "message": "$NAME$-instruksjoner", @@ -3799,7 +3810,7 @@ "message": "Multifaktorautentisering ble avbrutt" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "Ingen LastPass-data ble funnet" }, "incorrectUsernameOrPassword": { "message": "Feil brukernavn eller passord" @@ -3826,7 +3837,7 @@ "message": "Importerer kontoen din…" }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "LastPass-multifaktorautentisering kreves" }, "lastPassMFADesc": { "message": "Enter your one-time passcode from your authentication app" @@ -3992,7 +4003,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Suksess" }, "removePasskey": { "message": "Remove passkey" @@ -4094,13 +4105,13 @@ "message": "Admin Console" }, "accountSecurity": { - "message": "Account security" + "message": "Kontosikkerhet" }, "notifications": { - "message": "Notifications" + "message": "Varsler" }, "appearance": { - "message": "Appearance" + "message": "Utseende" }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." @@ -4129,7 +4140,7 @@ } }, "new": { - "message": "New" + "message": "Ny" }, "removeItem": { "message": "Remove $NAME$", @@ -4163,17 +4174,17 @@ "message": "Organization is deactivated" }, "owner": { - "message": "Owner" + "message": "Eier" }, "selfOwnershipLabel": { - "message": "You", + "message": "Du", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, "additionalInformation": { - "message": "Additional information" + "message": "Tilleggsinformasjon" }, "itemHistory": { "message": "Item history" @@ -4185,13 +4196,13 @@ "message": "Owner: You" }, "linked": { - "message": "Linked" + "message": "Tilknyttet" }, "copySuccessful": { "message": "Copy Successful" }, "upload": { - "message": "Upload" + "message": "Last opp" }, "addAttachment": { "message": "Add attachment" @@ -4227,7 +4238,7 @@ "message": "Free organizations cannot use attachments" }, "filters": { - "message": "Filters" + "message": "Filtre" }, "personalDetails": { "message": "Personal details" @@ -4236,7 +4247,7 @@ "message": "Identification" }, "contactInfo": { - "message": "Contact info" + "message": "Kontaktinfo" }, "downloadAttachment": { "message": "Download - $ITEMNAME$", @@ -4252,7 +4263,7 @@ "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { - "message": "Login credentials" + "message": "Legitimasjoner for innlogging" }, "authenticatorKey": { "message": "Authenticator key" @@ -4277,10 +4288,10 @@ "message": "Website added" }, "addWebsite": { - "message": "Add website" + "message": "Legg til nettsted" }, "deleteWebsite": { - "message": "Delete website" + "message": "Slett nettsted" }, "defaultLabel": { "message": "Default ($VALUE$)", @@ -4332,16 +4343,16 @@ } }, "enableAnimations": { - "message": "Enable animations" + "message": "Aktiver animasjoner" }, "showAnimations": { "message": "Show animations" }, "addAccount": { - "message": "Add account" + "message": "Legg til konto" }, "loading": { - "message": "Loading" + "message": "Laster" }, "data": { "message": "Data" @@ -4359,7 +4370,7 @@ "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { - "message": "Assign" + "message": "Knytt" }, "bulkCollectionAssignmentDialogDescriptionSingular": { "message": "Only organization members with access to these collections will be able to see the item." @@ -4383,7 +4394,7 @@ "message": "Add field" }, "add": { - "message": "Add" + "message": "Legg til" }, "fieldType": { "message": "Field type" @@ -4407,7 +4418,7 @@ "message": "Enter the the field's html id, name, aria-label, or placeholder." }, "editField": { - "message": "Edit field" + "message": "Rediger felt" }, "editFieldLabel": { "message": "Edit $LABEL$", @@ -4577,13 +4588,13 @@ "message": "Show number of login autofill suggestions on extension icon" }, "systemDefault": { - "message": "System default" + "message": "Systemforvalg" }, "enterprisePolicyRequirementsApplied": { "message": "Enterprise policy requirements have been applied to this setting" }, "retry": { - "message": "Retry" + "message": "Prøv igjen" }, "vaultCustomTimeoutMinimum": { "message": "Minimum custom timeout is 1 minute." @@ -4613,16 +4624,16 @@ "message": "Items that have been in trash more than 30 days will automatically be deleted" }, "restore": { - "message": "Restore" + "message": "Gjenopprett" }, "deleteForever": { - "message": "Delete forever" + "message": "Slett for alltid" }, "noEditPermissions": { "message": "You don't have permission to edit this item" }, "authenticating": { - "message": "Authenticating" + "message": "Autentiserer" }, "fillGeneratedPassword": { "message": "Fill generated password", @@ -4637,7 +4648,7 @@ "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "Mellomrom", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { @@ -4645,15 +4656,15 @@ "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "Baklengs apostrof", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "Utropstegn", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "Alfakrøll", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { @@ -4661,7 +4672,7 @@ "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "Dollartegn", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { @@ -4673,7 +4684,7 @@ "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "Prosenttegn", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { @@ -4689,7 +4700,7 @@ "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "Understrek", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { @@ -4729,11 +4740,11 @@ "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "Kolon", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Semikolon", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { @@ -4745,7 +4756,7 @@ "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Mindre enn", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { @@ -4753,7 +4764,7 @@ "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "Komma", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { @@ -4761,7 +4772,7 @@ "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Spørsmålstegn", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { @@ -4769,10 +4780,10 @@ "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "Små bokstaver" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "Store bokstaver" }, "generatedPassword": { "message": "Generated password" diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 3414760d2a3..b8075804229 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index fe415cfb1dc..1361b657f24 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "E-mailadres genereren" }, + "generatorBoundariesHint": { + "message": "Waarde moet tussen $MIN$ en $MAX$ liggen", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Type gebruikersnaam" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Inloggen op" - }, "opensInANewWindow": { "message": "Opent in een nieuw venster" }, diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 3414760d2a3..b8075804229 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 3414760d2a3..b8075804229 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 39f5541e596..c3c48b34e8a 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Wygenruj adres e-mail" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Rodzaj nazwy użytkownika" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logowanie do" - }, "opensInANewWindow": { "message": "Otwiera w nowym oknie" }, diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 63ab1eb3d8f..047ca09e7a4 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Tipo de usuário" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Entrando em" - }, "opensInANewWindow": { "message": "Abrir em uma nova janela" }, diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index a40a5ec1113..3885f6bad64 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Gerar e-mail" }, + "generatorBoundariesHint": { + "message": "O valor deve estar entre $MIN$ e $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Tipo de nome de utilizador" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "A iniciar sessão em" - }, "opensInANewWindow": { "message": "Abrir numa nova janela" }, diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 99bf86c9876..f25897f38f6 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Tip de nume de utilizator" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Conectare la" - }, "opensInANewWindow": { "message": "Se deschide într-o nouă fereastră" }, diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index e898d68ae86..7c7a70c82ff 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Сгенерировать email" }, + "generatorBoundariesHint": { + "message": "Значение должно быть между $MIN$ и $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Тип имени пользователя" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Войти на" - }, "opensInANewWindow": { "message": "Откроется в новом окне" }, diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 3ff7dde6897..4dd1bfeb898 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index e47d80bca26..dc46b0fdb85 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generovať e-mail" }, + "generatorBoundariesHint": { + "message": "Hodnota musí byť medzi $MIN$ a $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Typ používateľského mena" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Prihlásenie na" - }, "opensInANewWindow": { "message": "Otvárať v novom okne" }, diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 7ee7d1c5c77..d2af5a49008 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Vrsta uporabniškega imena" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Odpre se v novem oknu" }, diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index ad9ee20f295..b1dcec00b1a 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -20,16 +20,16 @@ "message": "Креирај налог" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Нови сте у Bitwarden-у?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Пријавите се са приступним кључем" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Употребити једнократну пријаву" }, "welcomeBack": { - "message": "Welcome back" + "message": "Добродошли назад" }, "setAStrongPassword": { "message": "Поставите јаку лозинку" @@ -120,7 +120,7 @@ "message": "Копирај лозинку" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Копирај приступну фразу" }, "copyNote": { "message": "Копирај белешку" @@ -168,7 +168,7 @@ "message": "Копирати белешке" }, "fill": { - "message": "Fill", + "message": "Попуни", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -427,7 +427,7 @@ "message": "Генерисање лозинке" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Генеришите приступну фразу" }, "regeneratePassword": { "message": "Поново генериши лозинку" @@ -591,7 +591,7 @@ "message": "Покрените веб локацију" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "Покренути сајт $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -846,7 +846,7 @@ "message": "Пријави се" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Пријавите се на Bitwarden" }, "restartRegistration": { "message": "Поново покрените регистрацију" @@ -1424,7 +1424,7 @@ "message": "УРЛ Сервера" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "УРЛ сервера који се самостално хостује", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1795,13 +1795,13 @@ "message": "Историја Лозинке" }, "generatorHistory": { - "message": "Generator history" + "message": "Генератор историје" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Испразнити генератор историје" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Ако наставите, сви уноси ће бити трајно избрисани из генератора историје. Да ли сте сигурни да желите да наставите?" }, "back": { "message": "Назад" @@ -1920,10 +1920,10 @@ "message": "Обриши историју" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Нема шта да се прикаже" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Недавно нисте ништа генерисали" }, "remove": { "message": "Уклони" @@ -2183,7 +2183,7 @@ "message": "Ваша нова главна лозинка не испуњава захтеве смерница." }, "receiveMarketingEmailsV2": { - "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." + "message": "Добијајте савете, најаве и могућности истраживања од Bitwarden-а у пријемном сандучету." }, "unsubscribe": { "message": "Одјави се" @@ -2512,7 +2512,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", + "message": "Додајте опционалну лозинку за примаоце да приступе овом Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2875,7 +2875,21 @@ "message": "Генериши име" }, "generateEmail": { - "message": "Generate email" + "message": "Генеришите имејл" + }, + "generatorBoundariesHint": { + "message": "Вредност мора бити између $MIN$ и $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } }, "usernameType": { "message": "Тип имена" @@ -2918,11 +2932,11 @@ "message": "Генеришите псеудоним е-поште помоћу екстерне услуге прослеђивања." }, "forwarderDomainName": { - "message": "Email domain", + "message": "Домен имејла", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "Изаберите домен који подржава изабрана услуга", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Пријављено на" - }, "opensInANewWindow": { "message": "Отвара се у новом прозору" }, @@ -3653,7 +3664,7 @@ "message": "Искачући додатак да бисте довршили пријаву." }, "popoutExtension": { - "message": "Popout extension" + "message": "Искачући додатак" }, "launchDuo": { "message": "Покренути DUO" @@ -3733,25 +3744,25 @@ "message": "Подаци из сефа су извезени" }, "typePasskey": { - "message": "Приступачни кључ" + "message": "Приступни кључ" }, "accessing": { "message": "Приступ" }, "passkeyNotCopied": { - "message": "Приступачни кључ неће бити копиран" + "message": "Приступни кључ неће бити копиран" }, "passkeyNotCopiedAlert": { - "message": "Приступачни кључ неће бити копиран на клонирану ставку. Да ли желите да наставите са клонирањем ставке?" + "message": "Приступни кључ неће бити копиран на клонирану ставку. Да ли желите да наставите са клонирањем ставке?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Верификацију захтева сајт који покреће. Ова функција још увек није имплементирана за налоге без главне лозинке." }, "logInWithPasskeyQuestion": { - "message": "Пријавите се са приступачним кључем?" + "message": "Пријавите се са приступним кључем?" }, "passkeyAlreadyExists": { - "message": "За ову апликацију већ постоји приступачни кључ." + "message": "За ову апликацију већ постоји приступни кључ." }, "noPasskeysFoundForThisApplication": { "message": "Нису пронађени приступни кључеви за ову апликацију." @@ -3781,7 +3792,7 @@ "message": "Изаберите приступни кључ за пријаву" }, "passkeyItem": { - "message": "Ставка приступачног кључа" + "message": "Ставка приступног кључа" }, "overwritePasskey": { "message": "Заменити приступни кључ?" @@ -3899,7 +3910,7 @@ "message": "сервер" }, "hostedAt": { - "message": "hosted at" + "message": "хостиран на" }, "useDeviceOrHardwareKey": { "message": "Користите свој уређај или хардверски кључ" @@ -3995,10 +4006,10 @@ "message": "Успех" }, "removePasskey": { - "message": "Уклонити приступачни кључ" + "message": "Уклонити приступни кључ" }, "passkeyRemoved": { - "message": "Приступачни кључ је уклоњен" + "message": "Приступни кључ је уклоњен" }, "autofillSuggestions": { "message": "Предлози за ауто-попуњавање" @@ -4347,7 +4358,7 @@ "message": "Подаци" }, "passkeys": { - "message": "Приступачни кључеви", + "message": "Приступни кључеви", "description": "A section header for a list of passkeys." }, "passwords": { @@ -4355,7 +4366,7 @@ "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Пријавите се са приступачним кључем", + "message": "Пријавите се са приступним кључем", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { @@ -4553,13 +4564,13 @@ "message": "Смештај ставке" }, "fileSend": { - "message": "File Send" + "message": "Датотека „Send“" }, "fileSends": { "message": "Датотека „Send“" }, "textSend": { - "message": "Text Send" + "message": "Текст „Send“" }, "textSends": { "message": "Текст „Send“" @@ -4592,7 +4603,7 @@ "message": "Додатни садржај је доступан" }, "fileSavedToDevice": { - "message": "File saved to device. Manage from your device downloads." + "message": "Датотека је сачувана на уређају. Управљајте преузимањима са свог уређаја." }, "showCharacterCount": { "message": "Прикажи бројање слова" @@ -4625,23 +4636,23 @@ "message": "Аутентификација" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "Попуните генерисану лозинку", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "Лозинка поново генерисана", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "Сачувати пријаву на Bitwarden?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "Простор", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "Тилда", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { @@ -4649,55 +4660,55 @@ "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "Узвичник", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "Знак „ет“", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { - "message": "Hash sign", + "message": "Знак „хеш“", "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "Знак долар", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "Знак постотак", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Caret", + "message": "Знак за уметање", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "Знак Ampersand", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "Знак звездица", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "Отворена заграда", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "Затворена заграда", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "Доња црта", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "Цртица", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "Плус", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 93a0b899a3c..e58a262d59c 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -147,7 +147,7 @@ "message": "Kopiera personnummer" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Kopiera passnummer" }, "copyLicenseNumber": { "message": "Copy license number" @@ -1511,7 +1511,7 @@ "message": "Komprometterade eller ej betrodda webbplatser kan utnyttja automatisk ifyllnad vid sidladdning." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "Läs mer om risker" }, "learnMoreAboutAutofill": { "message": "Läs mer om automatisk ifyllnad" @@ -2411,7 +2411,7 @@ "message": "Lösenordsskyddad" }, "copyLink": { - "message": "Copy link" + "message": "Kopiera länk" }, "copySendLink": { "message": "Kopiera Send-länk", @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Typ av användarnamn" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logga in på" - }, "opensInANewWindow": { "message": "Öppnas i ett nytt fönster" }, @@ -4613,7 +4624,7 @@ "message": "Items that have been in trash more than 30 days will automatically be deleted" }, "restore": { - "message": "Restore" + "message": "Återställ" }, "deleteForever": { "message": "Delete forever" @@ -4733,7 +4744,7 @@ "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Semikolon", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { @@ -4745,11 +4756,11 @@ "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Mindre än", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "Större än", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { @@ -4761,7 +4772,7 @@ "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Frågetecken", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 3414760d2a3..b8075804229 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 3b6ac3ebd15..9f111f9bec7 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 063dd63ede9..e49ada7538e 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "E-posta oluştur" }, + "generatorBoundariesHint": { + "message": "Değer $MIN$ ile $MAX$ arasında olmalıdır", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Kullanıcı adı türü" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Giriş yapılan konum" - }, "opensInANewWindow": { "message": "Yeni pencerede açılır" }, diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 25d12ca2600..01cdc4cab9e 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Генерувати е-пошту" }, + "generatorBoundariesHint": { + "message": "Значення має бути між $MIN$ та $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Тип імені користувача" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Увійти на" - }, "opensInANewWindow": { "message": "Відкривається у новому вікні" }, diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index cd390c6496e..61e211063de 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Loại tên người dùng" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Đang đăng nhập vào" - }, "opensInANewWindow": { "message": "Mở trong cửa sổ mới" }, diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 25baa71d7a1..e19cf428f42 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -20,16 +20,16 @@ "message": "创建账户" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Bitwarden 新手吗?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "使用通行密钥登录" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "使用单点登录" }, "welcomeBack": { - "message": "Welcome back" + "message": "欢迎回来" }, "setAStrongPassword": { "message": "设置强密码" @@ -120,7 +120,7 @@ "message": "复制密码" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "复制密码短语" }, "copyNote": { "message": "复制备注" @@ -168,7 +168,7 @@ "message": "复制备注" }, "fill": { - "message": "Fill", + "message": "填充", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -427,7 +427,7 @@ "message": "生成密码" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "生成密码短语" }, "regeneratePassword": { "message": "重新生成密码" @@ -591,7 +591,7 @@ "message": "启动网站" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "前往 $ITEMNAME$ 的网站", "placeholders": { "itemname": { "content": "$1", @@ -846,7 +846,7 @@ "message": "登录" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "登录到 Bitwarden" }, "restartRegistration": { "message": "重新开始注册" @@ -1424,7 +1424,7 @@ "message": "服务器 URL" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "自托管服务器 URL", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1795,13 +1795,13 @@ "message": "密码历史记录" }, "generatorHistory": { - "message": "Generator history" + "message": "生成器历史记录" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "清除生成器历史记录" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "若继续,所有条目将从生成器历史记录中永久删除。确定要继续吗?" }, "back": { "message": "后退" @@ -1920,10 +1920,10 @@ "message": "清除历史记录" }, "nothingToShow": { - "message": "Nothing to show" + "message": "没有可显示的内容" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "您最近没有生成任何内容" }, "remove": { "message": "移除" @@ -2512,7 +2512,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", + "message": "添加一个用于收件人访问此 Send 的可选密码。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2875,7 +2875,21 @@ "message": "生成用户名" }, "generateEmail": { - "message": "Generate email" + "message": "生成邮件地址" + }, + "generatorBoundariesHint": { + "message": "值必须在 $MIN$ 和 $MAX$ 之间", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } }, "usernameType": { "message": "用户名类型" @@ -2918,11 +2932,11 @@ "message": "使用外部转发服务生成一个电子邮件别名。" }, "forwarderDomainName": { - "message": "Email domain", + "message": "邮件域名", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "选择一个所选服务支持的域名", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "登录到" - }, "opensInANewWindow": { "message": "在新窗口中打开" }, @@ -4625,156 +4636,156 @@ "message": "正在验证" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "填充已生成的密码", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "密码已重新生成", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "将登录保存到 Bitwarden 吗?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "空格", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "波浪号", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "反引号", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "感叹号", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "艾特号", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { - "message": "Hash sign", + "message": "井号", "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "美元符号", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "百分号", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Caret", + "message": "脱字符", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "与和符", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "星号", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "左括号", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "右括号", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "下划线", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "连字符", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "加号", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "等号", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "左大括号", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "右大括号", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "左中括号", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "右中括号", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { - "message": "Pipe", + "message": "竖线", "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "反斜杠", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "冒号", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "分号", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "双引号", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "单引号", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "小于号", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "大于号", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "逗号", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "句号", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "问号", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "正斜杠", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "小写" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "大写" }, "generatedPassword": { - "message": "Generated password" + "message": "生成密码" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index f022d75a718..26bbbd0054f 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -168,7 +168,7 @@ "message": "複製備註" }, "fill": { - "message": "Fill", + "message": "填入", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -458,7 +458,7 @@ "description": "deprecated. Use specialCharactersLabel instead." }, "include": { - "message": "Include", + "message": "包含", "description": "Card header for password generator include block" }, "uppercaseDescription": { @@ -730,10 +730,10 @@ "message": "安全" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "確認主密碼" }, "masterPassword": { - "message": "Master password" + "message": "主密碼" }, "masterPassImportant": { "message": "Your master password cannot be recovered if you forget it!" @@ -1092,10 +1092,10 @@ "message": "This file export will be password protected and require the file password to decrypt." }, "filePassword": { - "message": "File password" + "message": "檔案密碼" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "此密碼將用於匯出和匯入此檔案" }, "accountRestrictedOptionDescription": { "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "使用者名稱類型" }, @@ -3220,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "正登入到" - }, "opensInANewWindow": { "message": "在新視窗開啟" }, @@ -3531,7 +3542,7 @@ "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { - "message": "New login", + "message": "新增登入資訊", "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts index 6865adca393..3084c3e5407 100644 --- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts +++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts @@ -1,7 +1,11 @@ import { matches, mock } from "jest-mock-extended"; import { BehaviorSubject, ReplaySubject, firstValueFrom, of, timeout } from "rxjs"; -import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { + Account, + AccountInfo, + AccountService, +} from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -14,7 +18,7 @@ import { AccountSwitcherService } from "./account-switcher.service"; describe("AccountSwitcherService", () => { let accountsSubject: BehaviorSubject>; - let activeAccountSubject: BehaviorSubject<{ id: UserId } & AccountInfo>; + let activeAccountSubject: BehaviorSubject; let authStatusSubject: ReplaySubject>; const accountService = mock(); @@ -29,7 +33,7 @@ describe("AccountSwitcherService", () => { beforeEach(() => { jest.resetAllMocks(); accountsSubject = new BehaviorSubject>(null); - activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null); + activeAccountSubject = new BehaviorSubject(null); authStatusSubject = new ReplaySubject>(1); // Use subject to allow for easy updates diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html index d5273fd9fb2..4a206b36fa8 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html @@ -9,7 +9,7 @@ - + diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index db85b28fa64..0301a76431d 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -15,6 +15,7 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { CurrentAccountComponent } from "../account-switching/current-account.component"; +import { AccountSwitcherService } from "../account-switching/services/account-switcher.service"; import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon"; @@ -50,6 +51,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { protected pageIcon: Icon; protected showReadonlyHostname: boolean; protected maxWidth: "md" | "3xl"; + protected hasLoggedInAccount: boolean = false; protected theme: string; protected logo = ExtensionBitwardenLogo; @@ -59,6 +61,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private i18nService: I18nService, private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService, + private accountSwitcherService: AccountSwitcherService, ) {} async ngOnInit(): Promise { @@ -68,6 +71,12 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { // Listen for page changes and update the page data appropriately this.listenForPageDataChanges(); this.listenForServiceDataChanges(); + + this.accountSwitcherService.availableAccounts$ + .pipe(takeUntil(this.destroy$)) + .subscribe((accounts) => { + this.hasLoggedInAccount = accounts.some((account) => account.id !== "addAccount"); + }); } private listenForPageDataChanges() { diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts index cc4aa2f7319..ad7e6f67361 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts @@ -27,6 +27,7 @@ import { ButtonModule, I18nMockService } from "@bitwarden/components"; import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon"; import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service"; +import { AccountSwitcherService } from "../account-switching/services/account-switcher.service"; import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service"; import { @@ -45,6 +46,7 @@ const decorators = (options: { applicationVersion?: string; clientType?: ClientType; hostName?: string; + accounts?: any[]; }) => { return [ componentWrapperDecorator( @@ -83,6 +85,13 @@ const decorators = (options: { }), }, }, + { + provide: AccountSwitcherService, + useValue: { + availableAccounts$: of(options.accounts || []), + SPECIAL_ADD_ACCOUNT_ID: "addAccount", + } as Partial, + }, { provide: AuthService, useValue: { @@ -300,3 +309,64 @@ export const DynamicContentExample: Story = { ], }), }; + +export const HasLoggedInAccountExample: Story = { + render: (args) => ({ + props: args, + template: "", + }), + decorators: decorators({ + components: [DefaultPrimaryOutletExampleComponent], + routes: [ + { + path: "**", + redirectTo: "has-logged-in-account", + pathMatch: "full", + }, + { + path: "", + component: ExtensionAnonLayoutWrapperComponent, + children: [ + { + path: "has-logged-in-account", + data: { + hasLoggedInAccount: true, + showAcctSwitcher: true, + }, + children: [ + { + path: "", + component: DefaultPrimaryOutletExampleComponent, + }, + { + path: "", + component: DefaultSecondaryOutletExampleComponent, + outlet: "secondary", + }, + { + path: "", + component: DefaultEnvSelectorOutletExampleComponent, + outlet: "environment-selector", + }, + ], + }, + ], + }, + ], + accounts: [ + { + name: "Test User", + email: "testuser@bitwarden.com", + id: "123e4567-e89b-12d3-a456-426614174000", + server: "bitwarden.com", + status: 2, + isActive: false, + }, + { + name: "addAccount", + id: "addAccount", + isActive: false, + }, + ], + }), +}; diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts index cd9dfc3702b..4d185fcbfc6 100644 --- a/apps/browser/src/auth/popup/home.component.ts +++ b/apps/browser/src/auth/popup/home.component.ts @@ -1,10 +1,12 @@ import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { Router } from "@angular/router"; -import { Subject, firstValueFrom, switchMap, takeUntil } from "rxjs"; +import { ActivatedRoute, Router } from "@angular/router"; +import { Subject, firstValueFrom, switchMap, takeUntil, tap } from "rxjs"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; @@ -38,9 +40,13 @@ export class HomeComponent implements OnInit, OnDestroy { private accountSwitcherService: AccountSwitcherService, private registerRouteService: RegisterRouteService, private toastService: ToastService, + private configService: ConfigService, + private route: ActivatedRoute, ) {} async ngOnInit(): Promise { + this.listenForUnauthUiRefreshFlagChanges(); + const email = await firstValueFrom(this.loginEmailService.loginEmail$); const rememberEmail = this.loginEmailService.getRememberEmail(); @@ -70,6 +76,29 @@ export class HomeComponent implements OnInit, OnDestroy { this.destroyed$.complete(); } + private listenForUnauthUiRefreshFlagChanges() { + this.configService + .getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh) + .pipe( + tap(async (flag) => { + // If the flag is turned ON, we must force a reload to ensure the correct UI is shown + if (flag) { + const uniqueQueryParams = { + ...this.route.queryParams, + // adding a unique timestamp to the query params to force a reload + t: new Date().getTime().toString(), + }; + + await this.router.navigate(["/login"], { + queryParams: uniqueQueryParams, + }); + } + }), + takeUntil(this.destroyed$), + ) + .subscribe(); + } + get availableAccounts$() { return this.accountSwitcherService.availableAccounts$; } diff --git a/apps/browser/src/auth/popup/settings/account-security-v1.component.ts b/apps/browser/src/auth/popup/settings/account-security-v1.component.ts index db45b3adb73..9d8a2ac4c88 100644 --- a/apps/browser/src/auth/popup/settings/account-security-v1.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security-v1.component.ts @@ -57,7 +57,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { availableVaultTimeoutActions: VaultTimeoutAction[] = []; vaultTimeoutOptions: VaultTimeoutOption[]; vaultTimeoutPolicyCallout: Observable<{ - timeout: { hours: number; minutes: number }; + timeout: { hours: string; minutes: string }; action: VaultTimeoutAction; }>; supportsBiometric: boolean; @@ -105,8 +105,8 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { let timeout; if (policy.data?.minutes) { timeout = { - hours: Math.floor(policy.data?.minutes / 60), - minutes: policy.data?.minutes % 60, + hours: Math.floor(policy.data?.minutes / 60).toString(), + minutes: (policy.data?.minutes % 60).toString(), }; } return { timeout: timeout, action: policy.data?.action }; diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index db50b784453..68d3f32b80f 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -216,7 +216,6 @@ export type OverlayBackgroundExtensionMessageHandlers = { getCurrentTabFrameId: ({ sender }: BackgroundSenderParam) => number; updateSubFrameData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; triggerSubFrameFocusInRebuild: ({ sender }: BackgroundSenderParam) => void; - shouldRepositionSubFrameInlineMenuOnScroll: ({ sender }: BackgroundSenderParam) => void; destroyAutofillInlineMenuListeners: ({ message, sender, diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index ebd73fa4cc0..6ec3c0a9b5a 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -32,6 +32,7 @@ import { } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -106,6 +107,7 @@ describe("OverlayBackground", () => { let selectedThemeMock$: BehaviorSubject; let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; let themeStateService: MockProxy; + let totpService: MockProxy; let overlayBackground: OverlayBackground; let portKeyForTabSpy: Record; let pageDetailsForTabSpy: PageDetailsForTab; @@ -184,6 +186,7 @@ describe("OverlayBackground", () => { inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); themeStateService = mock(); themeStateService.selectedTheme$ = selectedThemeMock$; + totpService = mock(); overlayBackground = new OverlayBackground( logService, cipherService, @@ -198,6 +201,7 @@ describe("OverlayBackground", () => { fido2ActiveRequestManager, inlineMenuFieldQualificationService, themeStateService, + totpService, generatedPasswordCallbackMock, addPasswordCallbackMock, ); @@ -629,6 +633,7 @@ describe("OverlayBackground", () => { it("skips updating the inline menu list if the user has the inline menu set to open on button click", async () => { inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick); + overlayBackground["inlineMenuListPort"] = null; tabsSendMessageSpy.mockImplementation((_tab, message, _options) => { if (message.command === "checkFocusedFieldHasValue") { return Promise.resolve(true); @@ -2264,7 +2269,7 @@ describe("OverlayBackground", () => { }); it("closes the list if the user has the inline menu set to show on button click and the list is open", async () => { - overlayBackground["isInlineMenuListVisible"] = true; + overlayBackground["inlineMenuListPort"] = listPortSpy; inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick); sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 6fb4589baa1..c42d1f7e640 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -33,6 +33,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; @@ -168,8 +169,6 @@ export class OverlayBackground implements OverlayBackgroundInterface { getCurrentTabFrameId: ({ sender }) => this.getSenderFrameId(sender), updateSubFrameData: ({ message, sender }) => this.updateSubFrameData(message, sender), triggerSubFrameFocusInRebuild: ({ sender }) => this.triggerSubFrameFocusInRebuild(sender), - shouldRepositionSubFrameInlineMenuOnScroll: ({ sender }) => - this.shouldRepositionSubFrameInlineMenuOnScroll(sender), destroyAutofillInlineMenuListeners: ({ message, sender }) => this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), @@ -219,6 +218,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private fido2ActiveRequestManager: Fido2ActiveRequestManager, private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService, private themeStateService: ThemeStateService, + private totpService: TotpService, private generatePasswordCallback: () => Promise, private addPasswordCallback: (password: string) => Promise, ) { @@ -1009,7 +1009,10 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.logService.error(error), ); - if ((await this.getInlineMenuVisibility()) === AutofillOverlayVisibility.OnButtonClick) { + if ( + !this.inlineMenuListPort && + (await this.getInlineMenuVisibility()) === AutofillOverlayVisibility.OnButtonClick + ) { return; } @@ -1057,7 +1060,6 @@ export class OverlayBackground implements OverlayBackgroundInterface { } const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId); - if (usePasskey && cipher.login?.hasFido2Credentials) { await this.authenticatePasskeyCredential( sender, @@ -1065,6 +1067,11 @@ export class OverlayBackground implements OverlayBackgroundInterface { ); this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher); + if (cipher.login?.totp) { + this.platformUtilsService.copyToClipboard( + await this.totpService.getCode(cipher.login.totp), + ); + } return; } @@ -1816,7 +1823,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { return; } - if (this.isInlineMenuListVisible) { + if (this.inlineMenuListPort) { this.closeInlineMenu(sender, { forceCloseInlineMenu: true, overlayElement: AutofillOverlayElement.List, @@ -2579,7 +2586,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param sender */ private resetFocusedFieldSubFrameOffsets(sender: chrome.runtime.MessageSender) { - if (this.focusedFieldData.frameId > 0 && this.subFrameOffsetsForTab[sender.tab.id]) { + if (this.focusedFieldData?.frameId > 0 && this.subFrameOffsetsForTab[sender.tab.id]) { this.subFrameOffsetsForTab[sender.tab.id].set(this.focusedFieldData.frameId, null); } } @@ -2592,24 +2599,11 @@ export class OverlayBackground implements OverlayBackgroundInterface { */ private async triggerSubFrameFocusInRebuild(sender: chrome.runtime.MessageSender) { this.cancelInlineMenuFadeInAndPositionUpdate(); + this.resetFocusedFieldSubFrameOffsets(sender); this.rebuildSubFrameOffsets$.next(sender); this.repositionInlineMenu$.next(sender); } - /** - * Triggers on scroll of a frame within the tab. Will reposition the inline menu - * if the focused field is within a sub-frame and the inline menu is visible. - * - * @param sender - The sender of the message - */ - private shouldRepositionSubFrameInlineMenuOnScroll(sender: chrome.runtime.MessageSender) { - if (!this.isInlineMenuButtonVisible || sender.tab.id !== this.focusedFieldData?.tabId) { - return false; - } - - return this.focusedFieldData.frameId > 0; - } - /** * Handles determining if the inline menu should be repositioned or closed, and initiates * the process of calculating the new position of the inline menu. diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts index 35930647921..cd22e1e5353 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts @@ -21,6 +21,7 @@ import AutofillInit from "./autofill-init"; domQueryService, domElementVisibilityService, inlineMenuFieldQualificationService, + inlineMenuContentService, ); windowContext.bitwardenAutofillInit = new AutofillInit( diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts index 174a695b769..11c8e4afd66 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts @@ -24,6 +24,7 @@ import AutofillInit from "./autofill-init"; domQueryService, domElementVisibilityService, inlineMenuFieldQualificationService, + inlineMenuContentService, ); windowContext.bitwardenAutofillInit = new AutofillInit( diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts index c9225b21ff9..fa43c928175 100644 --- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts @@ -1,5 +1,5 @@ import { EVENTS } from "@bitwarden/common/autofill/constants"; -import { ThemeType } from "@bitwarden/common/platform/enums"; +import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { setElementStyles } from "../../../utils"; import { @@ -210,19 +210,19 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf const { theme } = message; let borderColor: string; let verifiedTheme = theme; - if (verifiedTheme === ThemeType.System) { + if (verifiedTheme === ThemeTypes.System) { verifiedTheme = globalThis.matchMedia("(prefers-color-scheme: dark)").matches - ? ThemeType.Dark - : ThemeType.Light; + ? ThemeTypes.Dark + : ThemeTypes.Light; } - if (verifiedTheme === ThemeType.Dark) { + if (verifiedTheme === ThemeTypes.Dark) { borderColor = "#4c525f"; } - if (theme === ThemeType.Nord) { + if (theme === ThemeTypes.Nord) { borderColor = "#2E3440"; } - if (theme === ThemeType.SolarizedDark) { + if (theme === ThemeTypes.SolarizedDark) { borderColor = "#073642"; } if (borderColor) { diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index 6dfcac4abea..425d53783e1 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -1,7 +1,9 @@ +import { Theme } from "@bitwarden/common/platform/enums"; + type NotificationBarIframeInitData = { type?: string; isVaultLocked?: boolean; - theme?: string; + theme?: Theme; removeIndividualVault?: boolean; importType?: string; applyRedesign?: boolean; diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 3965ddfbce2..3c625297318 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -1,4 +1,4 @@ -import { ThemeType } from "@bitwarden/common/platform/enums"; +import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -392,10 +392,10 @@ function setupLogoLink(i18n: Record) { function setNotificationBarTheme() { let theme = notificationBarIframeInitData.theme; - if (theme === ThemeType.System) { + if (theme === ThemeTypes.System) { theme = globalThis.matchMedia("(prefers-color-scheme: dark)").matches - ? ThemeType.Dark - : ThemeType.Light; + ? ThemeTypes.Dark + : ThemeTypes.Light; } document.documentElement.classList.add(`theme_${theme}`); diff --git a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts index 5debf684979..b13db89ff59 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts @@ -1,5 +1,5 @@ import { EVENTS } from "@bitwarden/common/autofill/constants"; -import { ThemeType } from "@bitwarden/common/platform/enums"; +import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { sendExtensionMessage, setElementStyles } from "../../../utils"; import { @@ -239,19 +239,19 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe const { theme } = message; let borderColor: string; let verifiedTheme = theme; - if (verifiedTheme === ThemeType.System) { + if (verifiedTheme === ThemeTypes.System) { verifiedTheme = globalThis.matchMedia("(prefers-color-scheme: dark)").matches - ? ThemeType.Dark - : ThemeType.Light; + ? ThemeTypes.Dark + : ThemeTypes.Light; } - if (verifiedTheme === ThemeType.Dark) { + if (verifiedTheme === ThemeTypes.Dark) { borderColor = "#4c525f"; } - if (theme === ThemeType.Nord) { + if (theme === ThemeTypes.Nord) { borderColor = "#2E3440"; } - if (theme === ThemeType.SolarizedDark) { + if (theme === ThemeTypes.SolarizedDark) { borderColor = "#073642"; } if (borderColor) { diff --git a/apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts b/apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts index c97acd15d4c..82c03cacadf 100644 --- a/apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts +++ b/apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts @@ -1,6 +1,8 @@ +import { Theme } from "@bitwarden/common/platform/enums"; + export type NotificationTypeData = { isVaultLocked?: boolean; - theme?: string; + theme?: Theme; removeIndividualVault?: boolean; importType?: string; launchTimestamp?: number; diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index b395808f57a..ac247609b13 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -219,7 +219,11 @@ export class AutofillComponent implements OnInit { : AutofillOverlayVisibility.Off; await this.autofillSettingsService.setInlineMenuVisibility(newInlineMenuVisibilityValue); - await this.requestPrivacyPermission(); + + // No need to initiate browser permission request if a feature is being turned off + if (newInlineMenuVisibilityValue !== AutofillOverlayVisibility.Off) { + await this.requestPrivacyPermission(); + } } async updateAutofillOnPageLoad() { diff --git a/apps/browser/src/autofill/services/autofill-constants.ts b/apps/browser/src/autofill/services/autofill-constants.ts index f1758b6491c..55c3cced726 100644 --- a/apps/browser/src/autofill/services/autofill-constants.ts +++ b/apps/browser/src/autofill/services/autofill-constants.ts @@ -104,6 +104,7 @@ export class CreditCardAutoFillConstants { ]; static readonly CardHolderFieldNames: string[] = [ + "accountholdername", "cc-name", "card-name", "cardholder-name", @@ -113,6 +114,7 @@ export class CreditCardAutoFillConstants { ]; static readonly CardHolderFieldNameValues: string[] = [ + "accountholdername", "cc-name", "card-name", "cardholder-name", diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts index 91ad63955c7..8a77534d0d4 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts @@ -1,4 +1,4 @@ -import { mock } from "jest-mock-extended"; +import { mock, MockProxy } from "jest-mock-extended"; import { EVENTS } from "@bitwarden/common/autofill/constants"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -13,6 +13,7 @@ import { import AutofillField from "../models/autofill-field"; import AutofillForm from "../models/autofill-form"; import AutofillPageDetails from "../models/autofill-page-details"; +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service"; import { createAutofillFieldMock } from "../spec/autofill-mocks"; import { flushPromises, @@ -35,6 +36,7 @@ describe("AutofillOverlayContentService", () => { let domElementVisibilityService: DomElementVisibilityService; let autofillInit: AutofillInit; let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; + let inlineMenuContentService: MockProxy; let autofillOverlayContentService: AutofillOverlayContentService; let sendExtensionMessageSpy: jest.SpyInstance; const sendResponseSpy = jest.fn(); @@ -44,10 +46,12 @@ describe("AutofillOverlayContentService", () => { inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); domQueryService = new DomQueryService(); domElementVisibilityService = new DomElementVisibilityService(); + inlineMenuContentService = mock(); autofillOverlayContentService = new AutofillOverlayContentService( domQueryService, domElementVisibilityService, inlineMenuFieldQualificationService, + inlineMenuContentService, ); autofillInit = new AutofillInit( domQueryService, @@ -1699,6 +1703,10 @@ describe("AutofillOverlayContentService", () => { const repositionEvents = [EVENTS.SCROLL, EVENTS.RESIZE]; repositionEvents.forEach((repositionEvent) => { it(`sends a message trigger overlay reposition message to the background when a ${repositionEvent} event occurs`, async () => { + Object.defineProperty(globalThis, "scrollY", { + value: 10, + writable: true, + }); sendExtensionMessageSpy.mockResolvedValueOnce(true); globalThis.dispatchEvent(new Event(repositionEvent)); await flushPromises(); diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 645795d9f27..511e5dd594b 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -28,6 +28,7 @@ import { } from "../enums/autofill-overlay.enum"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service"; import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; import { currentlyInSandboxedIframe, @@ -155,6 +156,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ private domQueryService: DomQueryService, private domElementVisibilityService: DomElementVisibilityService, private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService, + private inlineMenuContentService?: AutofillInlineMenuContentService, ) {} /** @@ -1566,40 +1568,46 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ * the overlay elements on scroll or resize. */ private setOverlayRepositionEventListeners() { + let currentScrollY = globalThis.scrollY; + let currentScrollX = globalThis.scrollX; + let mostRecentTargetScrollY = 0; const repositionHandler = this.useEventHandlersMemo( throttle(this.handleOverlayRepositionEvent, 250), AUTOFILL_OVERLAY_HANDLE_REPOSITION, ); - const eventTargetContainsFocusedField = (eventTarget: Element | Document) => { - if (!eventTarget || !this.mostRecentlyFocusedField) { - return false; - } - - const activeElement = (eventTarget as Document).activeElement; - if (activeElement) { - return ( - activeElement === this.mostRecentlyFocusedField || - activeElement.contains(this.mostRecentlyFocusedField) - ); - } - + const eventTargetContainsFocusedField = (eventTarget: Element) => { if (typeof eventTarget.contains !== "function") { return false; } - return ( + + const targetScrollY = eventTarget.scrollTop; + if (targetScrollY === mostRecentTargetScrollY) { + return false; + } + + if ( eventTarget === this.mostRecentlyFocusedField || eventTarget.contains(this.mostRecentlyFocusedField) - ); + ) { + mostRecentTargetScrollY = targetScrollY; + return true; + } + + return false; }; const scrollHandler = this.useEventHandlersMemo( throttle(async (event) => { if ( - eventTargetContainsFocusedField(event.target) || - (await this.shouldRepositionSubFrameInlineMenuOnScroll()) + currentScrollY !== globalThis.scrollY || + currentScrollX !== globalThis.scrollX || + eventTargetContainsFocusedField(event.target) ) { repositionHandler(event); } + + currentScrollY = globalThis.scrollY; + currentScrollX = globalThis.scrollX; }, 50), AUTOFILL_OVERLAY_HANDLE_SCROLL, ); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 1ff2341d9f8..e79f6f69a36 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -436,9 +436,7 @@ export default class AutofillService implements AutofillServiceInterface { didAutofill = true; if (!options.skipLastUsed) { - // 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.cipherService.updateLastUsedDate(options.cipher.id); + await this.cipherService.updateLastUsedDate(options.cipher.id); } // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -940,13 +938,16 @@ export default class AutofillService implements AutofillServiceInterface { if (options.allowTotpAutofill) { await Promise.all( - totps.map(async (t) => { + totps.map(async (t, i) => { if (Object.prototype.hasOwnProperty.call(filledFields, t.opid)) { return; } filledFields[t.opid] = t; - const totpValue = await this.totpService.getCode(login.totp); + let totpValue = await this.totpService.getCode(login.totp); + if (totpValue.length == totps.length) { + totpValue = totpValue.charAt(i); + } AutofillService.fillByOpid(fillScript, t, totpValue); }), ); diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index fe7ebe61ee3..94d84997ee5 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -980,7 +980,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ const queueLength = this.mutationsQueue.length; if (!this.domQueryService.pageContainsShadowDomElements()) { - this.domQueryService.checkPageContainsShadowDom(); + this.checkPageContainsShadowDom(); } for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) { @@ -999,6 +999,29 @@ export class CollectAutofillContentService implements CollectAutofillContentServ this.mutationsQueue = []; }; + /** + * Handles checking if the current page contains a ShadowDOM element and + * flags that a re-collection of page details is required if it does. + */ + private checkPageContainsShadowDom() { + this.domQueryService.checkPageContainsShadowDom(); + if (this.domQueryService.pageContainsShadowDomElements()) { + this.flagPageDetailsUpdateIsRequired(); + } + } + + /** + * Triggers several flags that indicate that a collection of page details should + * occur again on a subsequent call after a mutation has been observed in the DOM. + */ + private flagPageDetailsUpdateIsRequired() { + this.domRecentlyMutated = true; + if (this.autofillOverlayContentService) { + this.autofillOverlayContentService.pageDetailsUpdateRequired = true; + } + this.noFieldsFound = false; + } + /** * Processes all mutation records encountered by the mutation observer. * @@ -1023,11 +1046,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ (this.isAutofillElementNodeMutated(mutation.removedNodes, true) || this.isAutofillElementNodeMutated(mutation.addedNodes)) ) { - this.domRecentlyMutated = true; - if (this.autofillOverlayContentService) { - this.autofillOverlayContentService.pageDetailsUpdateRequired = true; - } - this.noFieldsFound = false; + this.flagPageDetailsUpdateIsRequired(); return; } diff --git a/apps/browser/src/autofill/spec/autofill-mocks.ts b/apps/browser/src/autofill/spec/autofill-mocks.ts index a570119c72f..a4b6d700090 100644 --- a/apps/browser/src/autofill/spec/autofill-mocks.ts +++ b/apps/browser/src/autofill/spec/autofill-mocks.ts @@ -2,7 +2,7 @@ import { mock } from "jest-mock-extended"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; -import { ThemeType } from "@bitwarden/common/platform/enums"; +import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -210,7 +210,7 @@ export function createInitAutofillInlineMenuListMessageMock( command: "initAutofillInlineMenuList", translations: overlayPagesTranslations, styleSheetUrl: "https://jest-testing-website.com", - theme: ThemeType.Light, + theme: ThemeTypes.Light, authStatus: AuthenticationStatus.Unlocked, portKey: "portKey", inlineMenuFillType: CipherType.Login, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 6e2421e499f..fb92ebe04ae 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -257,12 +257,9 @@ import { BrowserPlatformUtilsService } from "../platform/services/platform-utils import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service"; import { BrowserSdkClientFactory } from "../platform/services/sdk/browser-sdk-client-factory"; import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service"; -import { ForegroundTaskSchedulerService } from "../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; -import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; import { OffscreenStorageService } from "../platform/storage/offscreen-storage.service"; -import { ForegroundSyncService } from "../platform/sync/foreground-sync.service"; import { SyncServiceListener } from "../platform/sync/sync-service.listener"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; @@ -401,7 +398,7 @@ export default class MainBackground { private popupViewCacheBackgroundService: PopupViewCacheBackgroundService; - constructor(public popupOnlyContext: boolean = false) { + constructor() { // Services const lockedCallback = async (userId?: string) => { if (this.notificationsService != null) { @@ -460,45 +457,6 @@ export default class MainBackground { this.offscreenDocumentService, ); - // Creates a session key for mv3 storage of large memory items - const sessionKey = new Lazy(async () => { - // Key already in session storage - const sessionStorage = new BrowserMemoryStorageService(); - const existingKey = await sessionStorage.get("session-key"); - if (existingKey) { - if (sessionStorage.valuesRequireDeserialization) { - return SymmetricCryptoKey.fromJSON(existingKey); - } - return existingKey; - } - - // New key - const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose( - 128, - "ephemeral", - "bitwarden-ephemeral", - ); - await sessionStorage.save("session-key", derivedKey); - return derivedKey; - }); - - const mv3MemoryStorageCreator = () => { - if (this.popupOnlyContext) { - return new ForegroundMemoryStorageService(); - } - - // For local backed session storage, we expect that the encrypted data on disk will persist longer than the encryption key in memory - // and failures to decrypt because of that are completely expected. For this reason, we pass in `false` to the `EncryptServiceImplementation` - // so that MAC failures are not logged. - return new LocalBackedSessionStorageService( - sessionKey, - this.storageService, - new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), - this.platformUtilsService, - this.logService, - ); - }; - this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used if (BrowserApi.isManifestVersion(3)) { @@ -506,18 +464,47 @@ export default class MainBackground { this.memoryStorageForStateProviders = new BrowserMemoryStorageService(); // mv3 stores to storage.session this.memoryStorageService = this.memoryStorageForStateProviders; } else { - if (popupOnlyContext) { - this.memoryStorageForStateProviders = new ForegroundMemoryStorageService(); - this.memoryStorageService = new ForegroundMemoryStorageService(); - } else { - this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(); // mv2 stores to memory - this.memoryStorageService = this.memoryStorageForStateProviders; - } + this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(); // mv2 stores to memory + this.memoryStorageService = this.memoryStorageForStateProviders; } - this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3) - ? mv3MemoryStorageCreator() // mv3 stores to local-backed session storage - : this.memoryStorageForStateProviders; // mv2 stores to the same location + if (BrowserApi.isManifestVersion(3)) { + // Creates a session key for mv3 storage of large memory items + const sessionKey = new Lazy(async () => { + // Key already in session storage + const sessionStorage = new BrowserMemoryStorageService(); + const existingKey = await sessionStorage.get("session-key"); + if (existingKey) { + if (sessionStorage.valuesRequireDeserialization) { + return SymmetricCryptoKey.fromJSON(existingKey); + } + return existingKey; + } + + // New key + const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose( + 128, + "ephemeral", + "bitwarden-ephemeral", + ); + await sessionStorage.save("session-key", derivedKey); + return derivedKey; + }); + + this.largeObjectMemoryStorageForStateProviders = new LocalBackedSessionStorageService( + sessionKey, + this.storageService, + // For local backed session storage, we expect that the encrypted data on disk will persist longer than the encryption key in memory + // and failures to decrypt because of that are completely expected. For this reason, we pass in `false` to the `EncryptServiceImplementation` + // so that MAC failures are not logged. + new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), + this.platformUtilsService, + this.logService, + ); + } else { + // mv2 stores to the same location + this.largeObjectMemoryStorageForStateProviders = this.memoryStorageForStateProviders; + } const localStorageStorageService = BrowserApi.isManifestVersion(3) ? new OffscreenStorageService(this.offscreenDocumentService) @@ -575,9 +562,10 @@ export default class MainBackground { this.derivedStateProvider, ); - this.taskSchedulerService = this.popupOnlyContext - ? new ForegroundTaskSchedulerService(this.logService, this.stateProvider) - : new BackgroundTaskSchedulerService(this.logService, this.stateProvider); + this.taskSchedulerService = new BackgroundTaskSchedulerService( + this.logService, + this.stateProvider, + ); this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.scheduleNextSyncInterval, () => this.fullSync(), ); @@ -632,6 +620,7 @@ export default class MainBackground { this.stateService, this.keyGenerationService, this.encryptService, + this.logService, ); this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); @@ -872,26 +861,24 @@ export default class MainBackground { this.vaultSettingsService = new VaultSettingsService(this.stateProvider); - if (!this.popupOnlyContext) { - this.vaultTimeoutService = new VaultTimeoutService( - this.accountService, - this.masterPasswordService, - this.cipherService, - this.folderService, - this.collectionService, - this.platformUtilsService, - this.messagingService, - this.searchService, - this.stateService, - this.authService, - this.vaultTimeoutSettingsService, - this.stateEventRunnerService, - this.taskSchedulerService, - this.logService, - lockedCallback, - logoutCallback, - ); - } + this.vaultTimeoutService = new VaultTimeoutService( + this.accountService, + this.masterPasswordService, + this.cipherService, + this.folderService, + this.collectionService, + this.platformUtilsService, + this.messagingService, + this.searchService, + this.stateService, + this.authService, + this.vaultTimeoutSettingsService, + this.stateEventRunnerService, + this.taskSchedulerService, + this.logService, + lockedCallback, + logoutCallback, + ); this.containerService = new ContainerService(this.keyService, this.encryptService); this.sendStateProvider = new SendStateProvider(this.stateProvider); @@ -912,59 +899,41 @@ export default class MainBackground { this.providerService = new ProviderService(this.stateProvider); - if (this.popupOnlyContext) { - this.syncService = new ForegroundSyncService( - this.stateService, - this.folderService, - this.folderApiService, - this.messagingService, - this.logService, - this.cipherService, - this.collectionService, - this.apiService, - this.accountService, - this.authService, - this.sendService, - this.sendApiService, - messageListener, - this.stateProvider, - ); - } else { - this.syncService = new DefaultSyncService( - this.masterPasswordService, - this.accountService, - this.apiService, - this.domainSettingsService, - this.folderService, - this.cipherService, - this.keyService, - this.collectionService, - this.messagingService, - this.policyService, - this.sendService, - this.logService, - this.keyConnectorService, - this.stateService, - this.providerService, - this.folderApiService, - this.organizationService, - this.sendApiService, - this.userDecryptionOptionsService, - this.avatarService, - logoutCallback, - this.billingAccountProfileStateService, - this.tokenService, - this.authService, - this.stateProvider, - ); + this.syncService = new DefaultSyncService( + this.masterPasswordService, + this.accountService, + this.apiService, + this.domainSettingsService, + this.folderService, + this.cipherService, + this.keyService, + this.collectionService, + this.messagingService, + this.policyService, + this.sendService, + this.logService, + this.keyConnectorService, + this.stateService, + this.providerService, + this.folderApiService, + this.organizationService, + this.sendApiService, + this.userDecryptionOptionsService, + this.avatarService, + logoutCallback, + this.billingAccountProfileStateService, + this.tokenService, + this.authService, + this.stateProvider, + ); + + this.syncServiceListener = new SyncServiceListener( + this.syncService, + messageListener, + this.messagingService, + this.logService, + ); - this.syncServiceListener = new SyncServiceListener( - this.syncService, - messageListener, - this.messagingService, - this.logService, - ); - } this.eventUploadService = new EventUploadService( this.apiService, this.stateProvider, @@ -1111,122 +1080,128 @@ export default class MainBackground { this.isSafari = this.platformUtilsService.isSafari(); // Background - if (!this.popupOnlyContext) { - this.fido2Background = new Fido2Background( - this.logService, - this.fido2ActiveRequestManager, - this.fido2ClientService, - this.vaultSettingsService, - this.scriptInjectorService, - this.configService, - this.authService, - ); - const lockService = new DefaultLockService(this.accountService, this.vaultTimeoutService); + this.fido2Background = new Fido2Background( + this.logService, + this.fido2ActiveRequestManager, + this.fido2ClientService, + this.vaultSettingsService, + this.scriptInjectorService, + this.configService, + this.authService, + ); - this.runtimeBackground = new RuntimeBackground( - this, - this.autofillService, - this.platformUtilsService as BrowserPlatformUtilsService, - this.notificationsService, - this.autofillSettingsService, - this.processReloadService, - this.environmentService, - this.messagingService, - this.logService, - this.configService, - messageListener, - this.accountService, - lockService, - ); - this.nativeMessagingBackground = new NativeMessagingBackground( - this.keyService, - this.encryptService, - this.cryptoFunctionService, - this.runtimeBackground, - this.messagingService, - this.appIdService, - this.platformUtilsService, - this.logService, - this.authService, - this.biometricStateService, - this.accountService, - ); - this.commandsBackground = new CommandsBackground( - this, - this.platformUtilsService, - this.vaultTimeoutService, - this.authService, - () => this.generatePasswordToClipboard(), - ); - this.notificationBackground = new NotificationBackground( - this.autofillService, - this.cipherService, - this.authService, - this.policyService, - this.folderService, - this.userNotificationSettingsService, - this.domainSettingsService, - this.environmentService, - this.logService, - this.themeStateService, - this.configService, - this.accountService, - ); + const lockService = new DefaultLockService(this.accountService, this.vaultTimeoutService); - this.overlayNotificationsBackground = new OverlayNotificationsBackground( - this.logService, - this.configService, - this.notificationBackground, - ); + this.runtimeBackground = new RuntimeBackground( + this, + this.autofillService, + this.platformUtilsService as BrowserPlatformUtilsService, + this.notificationsService, + this.autofillSettingsService, + this.processReloadService, + this.environmentService, + this.messagingService, + this.logService, + this.configService, + messageListener, + this.accountService, + lockService, + ); + this.nativeMessagingBackground = new NativeMessagingBackground( + this.keyService, + this.encryptService, + this.cryptoFunctionService, + this.runtimeBackground, + this.messagingService, + this.appIdService, + this.platformUtilsService, + this.logService, + this.authService, + this.biometricStateService, + this.accountService, + ); + this.commandsBackground = new CommandsBackground( + this, + this.platformUtilsService, + this.vaultTimeoutService, + this.authService, + () => this.generatePasswordToClipboard(), + ); + this.notificationBackground = new NotificationBackground( + this.autofillService, + this.cipherService, + this.authService, + this.policyService, + this.folderService, + this.userNotificationSettingsService, + this.domainSettingsService, + this.environmentService, + this.logService, + this.themeStateService, + this.configService, + this.accountService, + ); - this.filelessImporterBackground = new FilelessImporterBackground( - this.configService, - this.authService, - this.policyService, - this.notificationBackground, - this.importService, - this.syncService, - this.scriptInjectorService, - ); + this.overlayNotificationsBackground = new OverlayNotificationsBackground( + this.logService, + this.configService, + this.notificationBackground, + ); - this.autoSubmitLoginBackground = new AutoSubmitLoginBackground( - this.logService, - this.autofillService, - this.scriptInjectorService, - this.authService, - this.configService, - this.platformUtilsService, - this.policyService, - ); + this.filelessImporterBackground = new FilelessImporterBackground( + this.configService, + this.authService, + this.policyService, + this.notificationBackground, + this.importService, + this.syncService, + this.scriptInjectorService, + ); - const contextMenuClickedHandler = new ContextMenuClickedHandler( - (options) => this.platformUtilsService.copyToClipboard(options.text), - async () => this.generatePasswordToClipboard(), - async (tab, cipher) => { - this.loginToAutoFill = cipher; - if (tab == null) { - return; - } + this.autoSubmitLoginBackground = new AutoSubmitLoginBackground( + this.logService, + this.autofillService, + this.scriptInjectorService, + this.authService, + this.configService, + this.platformUtilsService, + this.policyService, + ); - // 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 - BrowserApi.tabSendMessage(tab, { - command: "collectPageDetails", - tab: tab, - sender: "contextMenu", - }); - }, - this.authService, - this.cipherService, - this.totpService, - this.eventCollectionService, - this.userVerificationService, - this.accountService, - ); + const contextMenuClickedHandler = new ContextMenuClickedHandler( + (options) => this.platformUtilsService.copyToClipboard(options.text), + async (_tab) => { + const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {}; + const password = await this.passwordGenerationService.generatePassword(options); + this.platformUtilsService.copyToClipboard(password); + // 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.passwordGenerationService.addHistory(password); + }, + async (tab, cipher) => { + this.loginToAutoFill = cipher; + if (tab == null) { + return; + } - this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler); - } + // 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 + BrowserApi.tabSendMessage(tab, { + command: "collectPageDetails", + tab: tab, + sender: "contextMenu", + }); + }, + this.authService, + this.cipherService, + this.totpService, + this.eventCollectionService, + this.userVerificationService, + this.accountService, + ); + + this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler); this.idleBackground = new IdleBackground( this.vaultTimeoutService, @@ -1245,29 +1220,27 @@ export default class MainBackground { this.stateProvider, ); - if (!this.popupOnlyContext) { - this.mainContextMenuHandler = new MainContextMenuHandler( - this.stateService, - this.autofillSettingsService, - this.i18nService, - this.logService, - this.billingAccountProfileStateService, - ); + this.mainContextMenuHandler = new MainContextMenuHandler( + this.stateService, + this.autofillSettingsService, + this.i18nService, + this.logService, + this.billingAccountProfileStateService, + ); - this.cipherContextMenuHandler = new CipherContextMenuHandler( - this.mainContextMenuHandler, - this.authService, + this.cipherContextMenuHandler = new CipherContextMenuHandler( + this.mainContextMenuHandler, + this.authService, + this.cipherService, + ); + + if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) { + this.webRequestBackground = new WebRequestBackground( + this.platformUtilsService, this.cipherService, + this.authService, + chrome.webRequest, ); - - if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) { - this.webRequestBackground = new WebRequestBackground( - this.platformUtilsService, - this.cipherService, - this.authService, - chrome.webRequest, - ); - } } this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.keyService); @@ -1282,7 +1255,7 @@ export default class MainBackground { this.containerService.attachToGlobal(self); // Only the "true" background should run migrations - await this.stateService.init({ runMigrations: !this.popupOnlyContext }); + await this.stateService.init({ runMigrations: true }); // This is here instead of in in the InitService b/c we don't plan for // side effects to run in the Browser InitService. @@ -1304,10 +1277,6 @@ export default class MainBackground { this.popupViewCacheBackgroundService.startObservingTabChanges(); - if (this.popupOnlyContext) { - return; - } - await this.vaultTimeoutService.init(true); this.fido2Background.init(); await this.runtimeBackground.init(); @@ -1347,14 +1316,17 @@ export default class MainBackground { if (flagEnabled("sdk")) { // Warn if the SDK for some reason can't be initialized let supported = false; + let error: Error; try { supported = await firstValueFrom(this.sdkService.supported$); } catch (e) { - // Do nothing. + error = e; } if (!supported) { - this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); + this.sdkService + .failedToInitialize("background", error) + .catch((e) => this.logService.error(e)); } } @@ -1633,7 +1605,6 @@ export default class MainBackground { */ async initOverlayAndTabsBackground() { if ( - this.popupOnlyContext || this.overlayBackground || this.tabsBackground || (await firstValueFrom(this.authService.activeAccountStatus$)) === @@ -1674,6 +1645,7 @@ export default class MainBackground { this.fido2ActiveRequestManager, inlineMenuFieldQualificationService, this.themeStateService, + this.totpService, () => this.generatePassword(), (password) => this.addPasswordToHistory(password), ); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 2399afd57b8..69f66dfa7c8 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -274,7 +274,11 @@ export class NativeMessagingBackground { let message = rawMessage as ReceiveMessage; if (!this.platformUtilsService.isSafari()) { message = JSON.parse( - await this.encryptService.decryptToUtf8(rawMessage as EncString, this.sharedSecret), + await this.encryptService.decryptToUtf8( + rawMessage as EncString, + this.sharedSecret, + "ipc-desktop-ipc-channel-key", + ), ); } diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 4997b368471..46aa3cefd70 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.10.999", + "version": "2024.11.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 3161f119a75..8118c212fc7 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.10.999", + "version": "2024.11.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/platform/background.ts b/apps/browser/src/platform/background.ts index a48c420e777..0cd9b3285e6 100644 --- a/apps/browser/src/platform/background.ts +++ b/apps/browser/src/platform/background.ts @@ -2,29 +2,6 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l import MainBackground from "../background/main.background"; -import { BrowserApi } from "./browser/browser-api"; - const logService = new ConsoleLogService(false); -if (BrowserApi.isManifestVersion(3)) { - startHeartbeat().catch((error) => logService.error(error)); -} const bitwardenMain = ((self as any).bitwardenMain = new MainBackground()); bitwardenMain.bootstrap().catch((error) => logService.error(error)); - -/** - * Tracks when a service worker was last alive and extends the service worker - * lifetime by writing the current time to extension storage every 20 seconds. - */ -async function runHeartbeat() { - await chrome.storage.local.set({ "last-heartbeat": new Date().getTime() }); -} - -/** - * Starts the heartbeat interval which keeps the service worker alive. - */ -async function startHeartbeat() { - // Run the heartbeat once at service worker startup, then again every 20 seconds. - runHeartbeat() - .then(() => setInterval(runHeartbeat, 20 * 1000)) - .catch((error) => logService.error(error)); -} diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index 33f18ce5723..072ef74004f 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -58,11 +58,33 @@ export class BrowserApi { } static async createWindow(options: chrome.windows.CreateData): Promise { - return new Promise((resolve) => - chrome.windows.create(options, (window) => { - resolve(window); - }), - ); + return new Promise((resolve) => { + chrome.windows.create(options, async (newWindow) => { + if (!BrowserApi.isSafariApi) { + return resolve(newWindow); + } + // Safari doesn't close the default extension popup when a new window is created so we need to + // manually trigger the close by focusing the main window after the new window is created + const allWindows = await new Promise((resolve) => { + chrome.windows.getAll({ windowTypes: ["normal"] }, (windows) => resolve(windows)); + }); + + const mainWindow = allWindows.find((window) => window.id !== newWindow.id); + + // No main window found, resolve the new window + if (mainWindow == null || !mainWindow.id) { + return resolve(newWindow); + } + + // Focus the main window to close the extension popup + chrome.windows.update(mainWindow.id, { focused: true }, () => { + // Refocus the newly created window + chrome.windows.update(newWindow.id, { focused: true }, () => { + resolve(newWindow); + }); + }); + }); + }); } /** diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html index 9c56271cf31..a5e08133fe2 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html @@ -3,27 +3,36 @@ diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index f12815b917c..c23da5ca7ce 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; -import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap, catchError, of } from "rxjs"; +import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -73,21 +73,24 @@ export class AppComponent implements OnInit, OnDestroy { ) { if (flagEnabled("sdk")) { // Warn if the SDK for some reason can't be initialized - this.sdkService.supported$ - .pipe( - takeUntilDestroyed(), - catchError(() => { - return of(false); - }), - ) - .subscribe((supported) => { + this.sdkService.supported$.pipe(takeUntilDestroyed()).subscribe({ + next: (supported) => { if (!supported) { this.logService.debug("SDK is not supported"); - this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); + this.sdkService + .failedToInitialize("popup", undefined) + .catch((e) => this.logService.error(e)); } else { this.logService.debug("SDK is supported"); } - }); + }, + error: (e: unknown) => { + this.sdkService + .failedToInitialize("popup", e as Error) + .catch((e) => this.logService.error(e)); + this.logService.error(e); + }, + }); } } diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 02ec66ec2f1..89b8816567d 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -6,6 +6,10 @@ margin: 0; } +html { + overflow: hidden; +} + html, body { font-family: $font-family-sans-serif; diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 91a13f4c0f0..1c73c2ac3de 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -24,8 +24,8 @@ import { LockComponentService, } from "@bitwarden/auth/angular"; import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -92,9 +92,15 @@ import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/imp import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service"; import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; +import { + FolderService as FolderServiceAbstraction, + InternalFolderService, +} from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { CompactModeService, DialogService, ToastService } from "@bitwarden/components"; @@ -107,7 +113,6 @@ import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extensio import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; import AutofillService from "../../autofill/services/autofill.service"; -import MainBackground from "../../background/main.background"; import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics"; import { BrowserKeyService } from "../../key-management/browser-key.service"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -117,13 +122,13 @@ import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sen /* eslint-enable no-restricted-imports */ import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document"; import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service"; -import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { PopupCompactModeService } from "../../platform/popup/layout/popup-compact-mode.service"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { PopupViewCacheService } from "../../platform/popup/view-cache/popup-view-cache.service"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; +import BrowserMemoryStorageService from "../../platform/services/browser-memory-storage.service"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; @@ -131,6 +136,7 @@ import { BrowserSdkClientFactory } from "../../platform/services/sdk/browser-sdk import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; +import { ForegroundSyncService } from "../../platform/sync/foreground-sync.service"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; import { ExtensionLockComponentService } from "../../services/extension-lock-component.service"; import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service"; @@ -152,26 +158,6 @@ const DISK_BACKUP_LOCAL_STORAGE = new SafeInjectionToken< AbstractStorageService & ObservableStorageService >("DISK_BACKUP_LOCAL_STORAGE"); -const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired(); -const mainBackground: MainBackground = needsBackgroundInit - ? createLocalBgService() - : BrowserApi.getBackgroundPage().bitwardenMain; - -function createLocalBgService() { - const localBgService = new MainBackground(true); - // 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 - localBgService.bootstrap(); - return localBgService; -} - -/** @deprecated This method needs to be removed as part of MV3 conversion. Please do not add more and actively try to remove usages */ -function getBgService(service: keyof MainBackground) { - return (): T => { - return mainBackground ? (mainBackground[service] as any as T) : null; - }; -} - /** * Provider definitions used in the ngModule. * Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety. @@ -308,8 +294,23 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: SyncService, - useFactory: getBgService("syncService"), - deps: [], + useClass: ForegroundSyncService, + deps: [ + StateService, + InternalFolderService, + FolderApiServiceAbstraction, + MessageSender, + LogService, + CipherService, + CollectionService, + ApiService, + AccountServiceAbstraction, + AuthService, + InternalSendService, + SendApiService, + MessageListener, + StateProvider, + ], }), safeProvider({ provide: DomainSettingsService, @@ -359,11 +360,6 @@ const safeProviders: SafeProvider[] = [ useClass: ForegroundVaultTimeoutService, deps: [MessagingServiceAbstraction], }), - safeProvider({ - provide: NotificationsService, - useFactory: getBgService("notificationsService"), - deps: [], - }), safeProvider({ provide: VaultFilterService, useClass: VaultFilterService, @@ -383,8 +379,8 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: MEMORY_STORAGE, - useFactory: getBgService("memoryStorageService"), - deps: [], + useFactory: (memoryStorage: AbstractStorageService) => memoryStorage, + deps: [OBSERVABLE_MEMORY_STORAGE], }), safeProvider({ provide: OBSERVABLE_MEMORY_STORAGE, @@ -393,9 +389,7 @@ const safeProviders: SafeProvider[] = [ return new ForegroundMemoryStorageService(); } - return getBgService( - "memoryStorageForStateProviders", - )(); + return new BrowserMemoryStorageService(); }, deps: [], }), @@ -408,9 +402,7 @@ const safeProviders: SafeProvider[] = [ return regularMemoryStorageService; } - return getBgService( - "largeObjectMemoryStorageForStateProviders", - )(); + return new ForegroundMemoryStorageService(); }, deps: [OBSERVABLE_MEMORY_STORAGE], }), @@ -495,15 +487,7 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: INTRAPROCESS_MESSAGING_SUBJECT, - useFactory: () => { - if (BrowserPopupUtils.backgroundInitializationRequired()) { - // There is no persistent main background which means we have one in memory, - // we need the same instance that our in memory background is utilizing. - return getBgService("intraprocessMessagingSubject")(); - } else { - return new Subject>>(); - } - }, + useFactory: () => new Subject>>(), deps: [], }), safeProvider({ @@ -515,23 +499,6 @@ const safeProviders: SafeProvider[] = [ ), deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService], }), - safeProvider({ - provide: INTRAPROCESS_MESSAGING_SUBJECT, - useFactory: () => { - if (needsBackgroundInit) { - // We will have created a popup within this context, in that case - // we want to make sure we have the same subject as that context so we - // can message with it. - return getBgService("intraprocessMessagingSubject")(); - } else { - // There isn't a locally created background so we will communicate with - // the true background through chrome apis, in that case, we can just create - // one for ourself. - return new Subject>>(); - } - }, - deps: [], - }), safeProvider({ provide: DISK_BACKUP_LOCAL_STORAGE, useFactory: (diskStorage: AbstractStorageService & ObservableStorageService) => @@ -573,13 +540,7 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: ForegroundTaskSchedulerService, - useFactory: (logService: LogService, stateProvider: StateProvider) => { - if (needsBackgroundInit) { - return getBgService("taskSchedulerService")(); - } - - return new ForegroundTaskSchedulerService(logService, stateProvider); - }, + useClass: ForegroundTaskSchedulerService, deps: [LogService, StateProvider], }), safeProvider({ diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts index 585f6067e3d..d1005883651 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts @@ -24,9 +24,9 @@ import { SendFormConfig, SendFormConfigService, SendFormMode, + SendFormModule, } 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"; diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts index fdf147b360f..1a3df238543 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts @@ -50,7 +50,7 @@ describe("SendCreatedComponent", () => { sendView = { id: sendId, - deletionDate: new Date(), + deletionDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), type: SendType.Text, accessId: "abc", urlB64Key: "123", @@ -127,8 +127,8 @@ describe("SendCreatedComponent", () => { it("should initialize send, daysAvailable, and hoursAvailable", () => { expect(component["send"]).toBe(sendView); - expect(component["daysAvailable"]).toBe(0); - expect(component["hoursAvailable"]).toBe(0); + expect(component["daysAvailable"]).toBe(7); + expect(component["hoursAvailable"]).toBe(168); }); it("should navigate back to the edit send form on close", async () => { @@ -140,7 +140,6 @@ describe("SendCreatedComponent", () => { describe("getHoursAvailable", () => { it("returns the correct number of hours", () => { - sendView.deletionDate.setDate(sendView.deletionDate.getDate() + 7); sendViewsSubject.next([sendView]); fixture.detectChanges(); @@ -150,7 +149,7 @@ describe("SendCreatedComponent", () => { describe("formatExpirationDate", () => { it("returns days plural if expiry is more than 24 hours", () => { - sendView.deletionDate.setDate(sendView.deletionDate.getDate() + 7); + sendView.deletionDate = new Date(Date.now() + 168 * 60 * 60 * 1000); sendViewsSubject.next([sendView]); fixture.detectChanges(); @@ -158,7 +157,7 @@ describe("SendCreatedComponent", () => { }); it("returns days singular if expiry is 24 hours", () => { - sendView.deletionDate.setDate(sendView.deletionDate.getDate() + 1); + sendView.deletionDate = new Date(Date.now() + 24 * 60 * 60 * 1000); sendViewsSubject.next([sendView]); fixture.detectChanges(); @@ -166,7 +165,7 @@ describe("SendCreatedComponent", () => { }); it("returns hours plural if expiry is more than 1 hour but less than 24", () => { - sendView.deletionDate.setHours(sendView.deletionDate.getHours() + 2); + sendView.deletionDate = new Date(Date.now() + 2 * 60 * 60 * 1000); sendViewsSubject.next([sendView]); fixture.detectChanges(); @@ -174,7 +173,7 @@ describe("SendCreatedComponent", () => { }); it("returns hours singular if expiry is in 1 hour", () => { - sendView.deletionDate.setHours(sendView.deletionDate.getHours() + 1); + sendView.deletionDate = new Date(Date.now() + 1 * 60 * 60 * 1000); sendViewsSubject.next([sendView]); fixture.detectChanges(); diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.html b/apps/browser/src/tools/popup/send-v2/send-v2.component.html index da803d5ae26..0a44d87b1b5 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.html @@ -6,7 +6,7 @@ -
+
{{ "sendDisabledWarning" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index 5febed788e7..8d95acbce9d 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -331,6 +331,8 @@ export class AddEditV2Component implements OnInit { return this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLocaleLowerCase()); case CipherType.SecureNote: return this.i18nService.t(partOne, this.i18nService.t("note").toLocaleLowerCase()); + case CipherType.SshKey: + return this.i18nService.t(partOne, this.i18nService.t("typeSshKey").toLocaleLowerCase()); } } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html index f4444a10aeb..973b1f9f1a4 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html @@ -88,3 +88,27 @@ [cipher]="cipher" > + + + + + + + + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts index a53c4a7c355..00a775024ce 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts @@ -48,5 +48,13 @@ export class ItemCopyActionsComponent { return !!this.cipher.notes; } + get hasSshKeyValues() { + return ( + !!this.cipher.sshKey.privateKey || + !!this.cipher.sshKey.publicKey || + !!this.cipher.sshKey.keyFingerprint + ); + } + constructor() {} } diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index f175c55c826..8ce3bcd2b60 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -1,7 +1,8 @@ import { CommonModule } from "@angular/common"; import { booleanAttribute, Component, Input, OnInit } from "@angular/core"; import { Router, RouterModule } from "@angular/router"; -import { firstValueFrom, map, Observable } from "rxjs"; +import { BehaviorSubject, firstValueFrom, map, switchMap } from "rxjs"; +import { filter } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -30,10 +31,18 @@ import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; imports: [ItemModule, IconButtonModule, MenuModule, CommonModule, JslibModule, RouterModule], }) export class ItemMoreOptionsComponent implements OnInit { + private _cipher$ = new BehaviorSubject(undefined); + @Input({ required: true, }) - cipher: CipherView; + set cipher(c: CipherView) { + this._cipher$.next(c); + } + + get cipher() { + return this._cipher$.value; + } /** * Flag to hide the autofill menu options. Used for items that are @@ -43,7 +52,15 @@ export class ItemMoreOptionsComponent implements OnInit { hideAutofillOptions: boolean; protected autofillAllowed$ = this.vaultPopupAutofillService.autofillAllowed$; - protected canClone$: Observable; + + /** + * Observable that emits a boolean value indicating if the user is authorized to clone the cipher. + * @protected + */ + protected canClone$ = this._cipher$.pipe( + filter((c) => c != null), + switchMap((c) => this.cipherAuthorizationService.canCloneCipher$(c)), + ); /** Boolean dependent on the current user having access to an organization */ protected hasOrganizations = false; @@ -63,7 +80,6 @@ export class ItemMoreOptionsComponent implements OnInit { async ngOnInit(): Promise { this.hasOrganizations = await this.organizationService.hasOrganizations(); - this.canClone$ = this.cipherAuthorizationService.canCloneCipher$(this.cipher); } get canEdit() { diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.html index 55674aa83e5..898d93da32c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.html @@ -3,6 +3,7 @@ [placeholder]="'search' | i18n" [(ngModel)]="searchText" (ngModelChange)="onSearchTextChanged()" + appAutofocus >
diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index cc2c419c04c..b1cbe8bc3e4 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -131,6 +131,8 @@ export class ViewV2Component { ); case CipherType.SecureNote: return this.i18nService.t("viewItemHeader", this.i18nService.t("note").toLowerCase()); + case CipherType.SshKey: + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeSshkey").toLowerCase()); } } diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.html b/apps/browser/src/vault/popup/components/vault/add-edit.component.html index 32d7c283b28..fb1efbbbd79 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.html +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.html @@ -529,6 +529,26 @@ />
+ + +
+
+ {{ "sshPrivateKey" | i18n }} + {{ cipher.sshKey.privateKey }} +
+
+ {{ "sshPublicKey" | i18n }} + {{ cipher.sshKey.publicKey }} +
+
+ {{ "sshKeyFingerprint" | i18n }} + {{ cipher.sshKey.keyFingerprint }} +
+
diff --git a/apps/browser/src/vault/popup/components/vault/vault-filter.component.html b/apps/browser/src/vault/popup/components/vault/vault-filter.component.html index f9ae340a89b..f5c28b2bebd 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-filter.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-filter.component.html @@ -114,6 +114,19 @@ {{ typeCounts.get(cipherType.SecureNote) || 0 }} +
diff --git a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts index ee6858fe441..27d36cbc2f1 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts @@ -106,6 +106,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn case CipherType.SecureNote: this.groupingTitle = this.i18nService.t("secureNotes"); break; + case CipherType.SshKey: + this.groupingTitle = this.i18nService.t("sshKeys"); + break; default: break; } diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html index e3f706db450..9eba5f8f906 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html @@ -23,7 +23,10 @@
- + diff --git a/apps/browser/src/vault/popup/components/vault/view.component.html b/apps/browser/src/vault/popup/components/vault/view.component.html index 73415c9070a..57a5d007d8a 100644 --- a/apps/browser/src/vault/popup/components/vault/view.component.html +++ b/apps/browser/src/vault/popup/components/vault/view.component.html @@ -429,6 +429,39 @@
{{ cipher.identity.country }}
+ +
+
+ + {{ "sshPrivateKey" | i18n }} + +
+
+
+ + {{ "sshPublicKey" | i18n }} + {{ cipher.sshKey.publicKey }} +
+
+ + {{ "sshFingerprint" | i18n }} + {{ cipher.sshKey.keyFingerprint }} +
+
diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 42d76e1dfe7..610d48fdc6f 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -202,6 +202,7 @@ describe("VaultPopupItemsService", () => { [CipherType.Card]: 2, [CipherType.Identity]: 3, [CipherType.SecureNote]: 4, + [CipherType.SshKey]: 5, }; // Assume all ciphers are autofill ciphers to test sorting diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 09c7d5fb0db..20ac3b3de96 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -277,6 +277,7 @@ export class VaultPopupItemsService { [CipherType.Card]: 2, [CipherType.Identity]: 3, [CipherType.SecureNote]: 4, + [CipherType.SshKey]: 5, }; // Compare types first diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 5d7e690193b..02ad7375f6a 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -97,6 +97,7 @@ describe("VaultPopupListFiltersService", () => { CipherType.Card, CipherType.Identity, CipherType.SecureNote, + CipherType.SshKey, ]); }); }); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 4059a43b56e..590807cff60 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -163,6 +163,11 @@ export class VaultPopupListFiltersService { label: this.i18nService.t("note"), icon: "bwi-sticky-note", }, + { + value: CipherType.SshKey, + label: this.i18nService.t("typeSshKey"), + icon: "bwi-key", + }, ]; /** Resets `filterForm` to the original state */ diff --git a/apps/browser/store/locales/nb/copy.resx b/apps/browser/store/locales/nb/copy.resx index 26a09cc855d..b496e223cbe 100644 --- a/apps/browser/store/locales/nb/copy.resx +++ b/apps/browser/store/locales/nb/copy.resx @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden passordbehandler At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. diff --git a/apps/cli/package.json b/apps/cli/package.json index 55bcee689d0..8ddb5daccd2 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.10.0", + "version": "2024.11.0", "keywords": [ "bitwarden", "password", @@ -58,7 +58,7 @@ "dependencies": { "@koa/multer": "3.0.2", "@koa/router": "13.1.0", - "argon2": "0.40.1", + "argon2": "0.41.1", "big-integer": "1.6.52", "browser-hrtime": "1.1.8", "chalk": "4.1.2", @@ -80,7 +80,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.52", + "tldts": "6.1.60", "zxcvbn": "4.4.2" } } diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index 9d7f7771dbc..3389d022e5e 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -68,7 +68,7 @@ export class UnlockCommand { return Response.error(e.message); } - const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); + const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey, userId); await this.keyService.setUserKey(userKey, userId); if (await this.keyConnectorService.getConvertAccountRequired()) { diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 3424e3fb28c..ae627e82e75 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -404,6 +404,7 @@ export class ServiceContainer { this.stateService, this.keyGenerationService, this.encryptService, + this.logService, ); this.kdfConfigService = new KdfConfigService(this.stateProvider); @@ -872,7 +873,7 @@ export class ServiceContainer { } if (!supported) { - this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); + this.sdkService.failedToInitialize("cli").catch((e) => this.logService.error(e)); } } } diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore index 040b2179fab..444c9a85100 100644 --- a/apps/desktop/.gitignore +++ b/apps/desktop/.gitignore @@ -2,3 +2,4 @@ dist-safari/ *.nupkg *.env PlugIns/safari.appex/ +xcuserdata/ diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index b87112508fe..53d151397f6 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -4,18 +4,28 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] [[package]] name = "aes" @@ -28,6 +38,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -39,9 +63,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "arboard" @@ -185,6 +209,28 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-task" version = "4.7.1" @@ -216,17 +262,17 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -235,12 +281,51 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bitwarden-russh" +version = "0.1.0" +source = "git+https://github.com/bitwarden/bitwarden-russh.git?branch=km/pm-10098/clean-russh-implementation#86ff1bf2f4620a3ae5684adee31abdbee33c6f07" +dependencies = [ + "anyhow", + "byteorder", + "futures", + "russh-cryptovec", + "ssh-encoding", + "ssh-key", + "thiserror", + "tokio", + "tokio-util", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -281,6 +366,16 @@ dependencies = [ "piper", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -289,9 +384,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cbc" @@ -304,9 +399,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.28" +version = "1.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" +checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" dependencies = [ "shlex", ] @@ -339,6 +434,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "cipher" version = "0.4.4" @@ -377,6 +483,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "convert_case" version = "0.6.0" @@ -388,9 +500,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" dependencies = [ "core-foundation-sys", "libc", @@ -438,10 +550,45 @@ dependencies = [ ] [[package]] -name = "cxx" -version = "1.0.128" +name = "ctr" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54ccead7d199d584d139148b04b4a368d1ec7556a1d9ea2548febb1b9d49f9a4" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxx" +version = "1.0.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbdc8cca144dce1c4981b5c9ab748761619979e515c3d53b5df385c677d1d007" dependencies = [ "cc", "cxxbridge-flags", @@ -451,9 +598,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.128" +version = "1.0.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77953e99f01508f89f55c494bfa867171ef3a6c8cea03d26975368f2121a5c1" +checksum = "c5764c3142ab44fcf857101d12c0ddf09c34499900557c764f5ad0597159d1fc" dependencies = [ "cc", "codespan-reporting", @@ -466,21 +613,32 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.128" +version = "1.0.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65777e06cc48f0cb0152024c77d6cf9e4bdb4408e7b48bea993d42fa0f5b02b6" +checksum = "d422aff542b4fa28c2ce8e5cc202d42dbf24702345c1fba3087b2d3f8a1b90ff" [[package]] name = "cxxbridge-macro" -version = "1.0.128" +version = "1.0.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98532a60dedaebc4848cb2cba5023337cc9ea3af16a5b062633fabfd9f18fb60" +checksum = "a1719100f31492cd6adeeab9a0f46cdbc846e615fdb66d7b398aa46ec7fdd06f" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -508,25 +666,38 @@ dependencies = [ "aes", "anyhow", "arboard", + "async-stream", "base64", + "bitwarden-russh", + "byteorder", "cbc", "core-foundation", "dirs", + "ed25519", "futures", "gio", + "homedir", "interprocess", "keytar", "libc", "libsecret", "log", + "pin-project", + "pkcs8", "rand", + "rand_chacha", "retry", + "rsa", + "russh-cryptovec", "scopeguard", "security-framework", "security-framework-sys", "sha2", + "ssh-encoding", + "ssh-key", "thiserror", "tokio", + "tokio-stream", "tokio-util", "typenum", "widestring", @@ -540,12 +711,16 @@ name = "desktop_napi" version = "0.0.0" dependencies = [ "anyhow", + "base64", "desktop_core", + "hex", "napi", "napi-build", "napi-derive", "tokio", + "tokio-stream", "tokio-util", + "windows-registry", ] [[package]] @@ -569,7 +744,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -614,6 +791,28 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2", + "subtle", +] + [[package]] name = "embed_plist" version = "1.2.2" @@ -696,6 +895,12 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -710,9 +915,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -758,9 +963,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210" dependencies = [ "fastrand", "futures-core", @@ -842,10 +1047,20 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.29.0" +name = "ghash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gio" @@ -884,7 +1099,7 @@ version = "0.19.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39650279f135469465018daae0ba53357942a5212137515777d5fdca74984a44" dependencies = [ - "bitflags", + "bitflags 2.6.0", "futures-channel", "futures-core", "futures-executor", @@ -936,9 +1151,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" [[package]] name = "heck" @@ -965,12 +1180,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "home" -version = "0.5.9" +name = "hmac" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "windows-sys 0.52.0", + "digest", +] + +[[package]] +name = "homedir" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bed305c13ce3829a09d627f5d43ff738482a09361ae4eb8039993b55fb10e5e" +dependencies = [ + "cfg-if", + "nix 0.26.4", + "widestring", + "windows", ] [[package]] @@ -1036,10 +1263,19 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.159" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.162" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libloading" @@ -1051,13 +1287,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", ] @@ -1124,6 +1366,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -1141,31 +1392,32 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "napi" -version = "2.16.11" +version = "2.16.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53575dfa17f208dd1ce3a2da2da4659aae393b256a472f2738a8586a6c4107fd" +checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b" dependencies = [ - "bitflags", + "bitflags 2.6.0", "ctor", "napi-derive", "napi-sys", @@ -1217,13 +1469,26 @@ dependencies = [ "libloading", ] +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + [[package]] name = "nix" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -1235,11 +1500,11 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cfg-if", "cfg_aliases 0.2.1", "libc", - "memoffset", + "memoffset 0.9.1", ] [[package]] @@ -1252,6 +1517,23 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1259,13 +1541,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] -name = "num_cpus" -version = "1.16.0" +name = "num-integer" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "hermit-abi 0.3.9", - "libc", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", ] [[package]] @@ -1299,7 +1601,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags", + "bitflags 2.6.0", "block2", "libc", "objc2", @@ -1315,7 +1617,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags", + "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", @@ -1345,7 +1647,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags", + "bitflags 2.6.0", "block2", "libc", "objc2", @@ -1357,7 +1659,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags", + "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", @@ -1369,7 +1671,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags", + "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", @@ -1391,6 +1693,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "option-ext" version = "0.2.0" @@ -1446,6 +1754,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "petgraph" version = "0.6.5" @@ -1457,10 +1784,30 @@ dependencies = [ ] [[package]] -name = "pin-project-lite" -version = "0.2.14" +name = "pin-project" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -1479,6 +1826,44 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "pkcs5", + "rand_core", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.31" @@ -1500,6 +1885,29 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1526,9 +1934,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -1593,7 +2001,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -1609,9 +2017,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -1645,6 +2053,37 @@ dependencies = [ "rand", ] +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "russh-cryptovec" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadd2c0ab350e21c66556f94ee06f766d8bdae3213857ba7610bfd8e10e51880" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1652,18 +2091,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] -name = "rustix" -version = "0.38.34" +name = "rustc_version" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "bitflags", + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1683,12 +2140,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" [[package]] -name = "security-framework" -version = "2.11.0" +name = "scrypt" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ - "bitflags", + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "security-framework" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71" +dependencies = [ + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -1697,9 +2165,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -1713,18 +2181,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", @@ -1788,6 +2256,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "simplelog" version = "0.12.2" @@ -1824,6 +2302,69 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "aes", + "aes-gcm", + "cbc", + "chacha20", + "cipher", + "ctr", + "poly1305", + "ssh-encoding", + "subtle", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2", +] + +[[package]] +name = "ssh-key" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca9b366a80cf18bb6406f4cf4d10aebfb46140a8c0c33f666a144c5c76ecbafc" +dependencies = [ + "bcrypt-pbkdf", + "ed25519-dalek", + "num-bigint-dig", + "rand_core", + "rsa", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -1831,10 +2372,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "syn" -version = "2.0.79" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -1862,9 +2409,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", @@ -1884,18 +2431,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -1937,26 +2484,25 @@ dependencies = [ [[package]] name = "tokio" -version = "1.38.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -1964,10 +2510,21 @@ dependencies = [ ] [[package]] -name = "tokio-util" -version = "0.7.11" +name = "tokio-stream" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -2043,12 +2600,11 @@ dependencies = [ [[package]] name = "tree_magic_mini" -version = "3.1.5" +version = "3.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469a727cac55b41448315cc10427c069c618ac59bb6a4480283fcd811749bdc2" +checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63" dependencies = [ "fnv", - "home", "memchr", "nom", "once_cell", @@ -2067,7 +2623,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ - "memoffset", + "memoffset 0.9.1", "tempfile", "winapi", ] @@ -2090,6 +2646,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "version-compare" version = "0.2.0" @@ -2124,11 +2690,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.6" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3f45d1222915ef1fd2057220c1d9d9624b7654443ea35c3877f7a52bd0a5a2d" +checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" dependencies = [ - "bitflags", + "bitflags 2.6.0", "rustix", "wayland-backend", "wayland-scanner", @@ -2140,7 +2706,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -2152,7 +2718,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ - "bitflags", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -2236,7 +2802,7 @@ checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ "windows-implement", "windows-interface", - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -2262,6 +2828,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-registry" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafa604f2104cf5ae2cc2db1dee84b7e6a5d11b05f737b60def0ffdc398cbc0a" +dependencies = [ + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -2271,6 +2848,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978d65aedf914c664c510d9de43c8fd85ca745eaff1ed53edf409b479e441663" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -2477,9 +3072,9 @@ dependencies = [ [[package]] name = "zbus" -version = "4.3.1" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "851238c133804e0aa888edf4a0229481c753544ca12a60fd1c3230c8a500fe40" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" dependencies = [ "async-broadcast", "async-executor", @@ -2515,9 +3110,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.3.1" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d5a3f12c20bd473be3194af6b49d50d7bb804ef3192dc70eddedb26b85d9da7" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2572,10 +3167,16 @@ dependencies = [ ] [[package]] -name = "zvariant" -version = "4.1.2" +name = "zeroize" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1724a2b330760dc7d2a8402d841119dc869ef120b139d29862d6980e9c75bfc9" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" dependencies = [ "endi", "enumflags2", @@ -2586,9 +3187,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "4.1.2" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55025a7a518ad14518fb243559c058a2e5b848b015e31f1d90414f36e3317859" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2599,9 +3200,9 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc242db087efc22bd9ade7aa7809e4ba828132edc312871584a6b4391bdf8786" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 2a6e402b445..4f6fdb47fdf 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -23,25 +23,43 @@ sys = [ [dependencies] aes = "=0.8.4" -anyhow = "=1.0.86" +anyhow = "=1.0.93" arboard = { version = "=3.4.1", default-features = false, features = [ "wayland-data-control", ] } +async-stream = "0.3.5" base64 = "=0.22.1" +byteorder = "1.5.0" cbc = { version = "=0.1.2", features = ["alloc"] } +homedir = "0.3.3" +libc = "=0.2.162" +pin-project = "1.1.5" dirs = "=5.0.1" -futures = "=0.3.30" +futures = "=0.3.31" interprocess = { version = "=2.2.1", features = ["tokio"] } -libc = "=0.2.159" log = "=0.4.22" rand = "=0.8.5" retry = "=2.0.0" +russh-cryptovec = "0.7.3" scopeguard = "=1.2.0" sha2 = "=0.10.8" -thiserror = "=1.0.61" -tokio = { version = "=1.38.0", features = ["io-util", "sync", "macros"] } -tokio-util = "=0.7.11" +ssh-encoding = "0.2.0" +ssh-key = { version = "0.6.6", default-features = false, features = [ + "encryption", + "ed25519", + "rsa", + "getrandom", +] } +bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", branch = "km/pm-10098/clean-russh-implementation" } +tokio = { version = "=1.40.0", features = ["io-util", "sync", "macros", "net"] } +tokio-stream = { version = "=0.1.15", features = ["net"] } +tokio-util = "=0.7.12" +thiserror = "=1.0.69" typenum = "=1.17.0" +rand_chacha = "=0.3.1" +pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] } +rsa = "=0.9.6" +ed25519 = { version = "=2.2.3", features = ["pkcs8"] } [target.'cfg(windows)'.dependencies] widestring = { version = "=1.1.0", optional = true } @@ -61,12 +79,12 @@ windows = { version = "=0.57.0", features = [ keytar = "=0.1.6" [target.'cfg(target_os = "macos")'.dependencies] -core-foundation = { version = "=0.9.4", optional = true } -security-framework = { version = "=2.11.0", optional = true } -security-framework-sys = { version = "=2.11.0", optional = true } +core-foundation = { version = "=0.10.0", optional = true } +security-framework = { version = "=3.0.0", optional = true } +security-framework-sys = { version = "=2.12.0", optional = true } [target.'cfg(target_os = "linux")'.dependencies] gio = { version = "=0.19.5", optional = true } libsecret = { version = "=0.5.0", optional = true } -zbus = { version = "=4.3.1", optional = true } +zbus = { version = "=4.4.0", optional = true } zbus_polkit = { version = "=4.0.0", optional = true } diff --git a/apps/desktop/desktop_native/core/src/biometric/mod.rs b/apps/desktop/desktop_native/core/src/biometric/mod.rs index c41ad9dda53..72352cf2288 100644 --- a/apps/desktop/desktop_native/core/src/biometric/mod.rs +++ b/apps/desktop/desktop_native/core/src/biometric/mod.rs @@ -6,8 +6,8 @@ use anyhow::{anyhow, Result}; #[cfg_attr(target_os = "macos", path = "macos.rs")] mod biometric; -pub use biometric::Biometric; use base64::{engine::general_purpose::STANDARD as base64_engine, Engine}; +pub use biometric::Biometric; use sha2::{Digest, Sha256}; use crate::crypto::{self, CipherString}; @@ -42,7 +42,6 @@ pub trait BiometricTrait { ) -> Result; } - fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result { let iv = base64_engine .decode(iv_b64)? @@ -77,4 +76,4 @@ impl KeyMaterial { pub fn derive_key(&self) -> Result> { Ok(Sha256::digest(self.digest_material())) } -} \ No newline at end of file +} diff --git a/apps/desktop/desktop_native/core/src/biometric/unix.rs b/apps/desktop/desktop_native/core/src/biometric/unix.rs index 742b736e812..563bd1dfe52 100644 --- a/apps/desktop/desktop_native/core/src/biometric/unix.rs +++ b/apps/desktop/desktop_native/core/src/biometric/unix.rs @@ -5,13 +5,13 @@ use base64::Engine; use rand::RngCore; use sha2::{Digest, Sha256}; -use crate::biometric::{KeyMaterial, OsDerivedKey, base64_engine}; +use crate::biometric::{base64_engine, KeyMaterial, OsDerivedKey}; use zbus::Connection; use zbus_polkit::policykit1::*; use super::{decrypt, encrypt}; -use anyhow::anyhow; use crate::crypto::CipherString; +use anyhow::anyhow; /// The Unix implementation of the biometric trait. pub struct Biometric {} @@ -22,13 +22,15 @@ impl super::BiometricTrait for Biometric { let proxy = AuthorityProxy::new(&connection).await?; let subject = Subject::new_for_owner(std::process::id(), None, None)?; let details = std::collections::HashMap::new(); - let result = proxy.check_authorization( - &subject, - "com.bitwarden.Bitwarden.unlock", - &details, - CheckAuthorizationFlags::AllowUserInteraction.into(), - "", - ).await; + let result = proxy + .check_authorization( + &subject, + "com.bitwarden.Bitwarden.unlock", + &details, + CheckAuthorizationFlags::AllowUserInteraction.into(), + "", + ) + .await; match result { Ok(result) => { @@ -106,4 +108,4 @@ fn random_challenge() -> [u8; 16] { let mut challenge = [0u8; 16]; rand::thread_rng().fill_bytes(&mut challenge); challenge -} \ No newline at end of file +} diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs index c5db9e3277b..d5e8b6dc915 100644 --- a/apps/desktop/desktop_native/core/src/biometric/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs @@ -160,7 +160,6 @@ impl super::BiometricTrait for Biometric { } } - fn random_challenge() -> [u8; 16] { let mut challenge = [0u8; 16]; rand::thread_rng().fill_bytes(&mut challenge); diff --git a/apps/desktop/desktop_native/core/src/ipc/mod.rs b/apps/desktop/desktop_native/core/src/ipc/mod.rs index c7ac1a43404..d406b6aa137 100644 --- a/apps/desktop/desktop_native/core/src/ipc/mod.rs +++ b/apps/desktop/desktop_native/core/src/ipc/mod.rs @@ -31,7 +31,7 @@ pub fn path(name: &str) -> std::path::PathBuf { format!(r"\\.\pipe\{hash_b64}.app.{name}").into() } - #[cfg(target_os = "macos")] + #[cfg(all(target_os = "macos", not(debug_assertions)))] { let mut home = dirs::home_dir().unwrap(); @@ -53,6 +53,13 @@ pub fn path(name: &str) -> std::path::PathBuf { tmp.join(format!("app.{name}")) } + #[cfg(all(target_os = "macos", debug_assertions))] + { + // When running in debug mode, we use the tmp dir because the app is not sandboxed + let dir = std::env::temp_dir(); + dir.join(format!("app.{name}")) + } + #[cfg(target_os = "linux")] { // On Linux, we use the user's cache directory. diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs index 3132c56f7f8..f38e6ef97b4 100644 --- a/apps/desktop/desktop_native/core/src/lib.rs +++ b/apps/desktop/desktop_native/core/src/lib.rs @@ -11,3 +11,6 @@ pub mod password; pub mod process_isolation; #[cfg(feature = "sys")] pub mod powermonitor; +#[cfg(feature = "sys")] + +pub mod ssh_agent; diff --git a/apps/desktop/desktop_native/core/src/password/unix.rs b/apps/desktop/desktop_native/core/src/password/unix.rs index 53053ee467e..1817a4d62ee 100644 --- a/apps/desktop/desktop_native/core/src/password/unix.rs +++ b/apps/desktop/desktop_native/core/src/password/unix.rs @@ -41,7 +41,11 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> { } pub fn is_available() -> Result { - let result = password_clear_sync(Some(&get_schema()), build_attributes("bitwardenSecretsAvailabilityTest", "test"), gio::Cancellable::NONE); + let result = password_clear_sync( + Some(&get_schema()), + build_attributes("bitwardenSecretsAvailabilityTest", "test"), + gio::Cancellable::NONE, + ); match result { Ok(_) => Ok(true), Err(_) => { diff --git a/apps/desktop/desktop_native/core/src/powermonitor/linux.rs b/apps/desktop/desktop_native/core/src/powermonitor/linux.rs index fe07ad11ff8..7d0fde15ed4 100644 --- a/apps/desktop/desktop_native/core/src/powermonitor/linux.rs +++ b/apps/desktop/desktop_native/core/src/powermonitor/linux.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use zbus::{Connection, MatchRule, export::futures_util::TryStreamExt}; +use zbus::{export::futures_util::TryStreamExt, Connection, MatchRule}; struct ScreenLock { interface: Cow<'static, str>, path: Cow<'static, str>, @@ -42,7 +42,15 @@ pub async fn on_lock(tx: tokio::sync::mpsc::Sender<()>) -> Result<(), Box bool { let connection = Connection::session().await.unwrap(); for monitor in SCREEN_LOCK_MONITORS { - let res = connection.call_method(Some(monitor.interface.clone()), monitor.path.clone(), Some(monitor.interface.clone()), "GetActive", &()).await; + let res = connection + .call_method( + Some(monitor.interface.clone()), + monitor.path.clone(), + Some(monitor.interface.clone()), + "GetActive", + &(), + ) + .await; if res.is_ok() { return true; } diff --git a/apps/desktop/desktop_native/core/src/process_isolation/linux.rs b/apps/desktop/desktop_native/core/src/process_isolation/linux.rs index ba8734cff7f..dc027e0b546 100644 --- a/apps/desktop/desktop_native/core/src/process_isolation/linux.rs +++ b/apps/desktop/desktop_native/core/src/process_isolation/linux.rs @@ -1,7 +1,7 @@ use anyhow::Result; -use libc::{c_int, self}; #[cfg(target_env = "gnu")] use libc::c_uint; +use libc::{self, c_int}; // RLIMIT_CORE is the maximum size of a core dump file. Setting both to 0 disables core dumps, on crashes // https://github.com/torvalds/linux/blob/1613e604df0cd359cf2a7fbd9be7a0bcfacfabd0/include/uapi/asm-generic/resource.h#L20 @@ -22,7 +22,10 @@ pub fn disable_coredumps() -> Result<()> { }; if unsafe { libc::setrlimit(RLIMIT_CORE, &rlimit) } != 0 { let e = std::io::Error::last_os_error(); - return Err(anyhow::anyhow!("failed to disable core dumping, memory might be persisted to disk on crashes {}", e)) + return Err(anyhow::anyhow!( + "failed to disable core dumping, memory might be persisted to disk on crashes {}", + e + )); } Ok(()) @@ -35,7 +38,7 @@ pub fn is_core_dumping_disabled() -> Result { }; if unsafe { libc::getrlimit(RLIMIT_CORE, &mut rlimit) } != 0 { let e = std::io::Error::last_os_error(); - return Err(anyhow::anyhow!("failed to get core dump limit {}", e)) + return Err(anyhow::anyhow!("failed to get core dump limit {}", e)); } Ok(rlimit.rlim_cur == 0 && rlimit.rlim_max == 0) @@ -44,7 +47,10 @@ pub fn is_core_dumping_disabled() -> Result { pub fn disable_memory_access() -> Result<()> { if unsafe { libc::prctl(PR_SET_DUMPABLE, 0) } != 0 { let e = std::io::Error::last_os_error(); - return Err(anyhow::anyhow!("failed to disable memory dumping, memory is dumpable by other processes {}", e)) + return Err(anyhow::anyhow!( + "failed to disable memory dumping, memory is dumpable by other processes {}", + e + )); } Ok(()) diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs b/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs new file mode 100644 index 00000000000..fe639f20e7f --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs @@ -0,0 +1,45 @@ +use rand::SeedableRng; +use rand_chacha::ChaCha8Rng; +use ssh_key::{Algorithm, HashAlg, LineEnding}; + +use super::importer::SshKey; + +pub async fn generate_keypair(key_algorithm: String) -> Result { + // sourced from cryptographically secure entropy source, with sources for all targets: https://docs.rs/getrandom + // if it cannot be securely sourced, this will panic instead of leading to a weak key + let mut rng: ChaCha8Rng = ChaCha8Rng::from_entropy(); + + let key = match key_algorithm.as_str() { + "ed25519" => ssh_key::PrivateKey::random(&mut rng, Algorithm::Ed25519), + "rsa2048" | "rsa3072" | "rsa4096" => { + let bits = match key_algorithm.as_str() { + "rsa2048" => 2048, + "rsa3072" => 3072, + "rsa4096" => 4096, + _ => return Err(anyhow::anyhow!("Unsupported RSA key size")), + }; + let rsa_keypair = ssh_key::private::RsaKeypair::random(&mut rng, bits) + .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?; + + let private_key = ssh_key::PrivateKey::new( + ssh_key::private::KeypairData::from(rsa_keypair), + "".to_string(), + ) + .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?; + Ok(private_key) + } + _ => { + return Err(anyhow::anyhow!("Unsupported key algorithm")); + } + } + .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?; + + let private_key_openssh = key + .to_openssh(LineEnding::LF) + .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?; + Ok(SshKey { + private_key: private_key_openssh.to_string(), + public_key: key.public_key().to_string(), + key_fingerprint: key.fingerprint(HashAlg::Sha256).to_string(), + }) +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs b/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs new file mode 100644 index 00000000000..3d643e764c7 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs @@ -0,0 +1,395 @@ +use ed25519; +use pkcs8::{ + der::Decode, EncryptedPrivateKeyInfo, ObjectIdentifier, PrivateKeyInfo, SecretDocument, +}; +use ssh_key::{ + private::{Ed25519Keypair, Ed25519PrivateKey, RsaKeypair}, + HashAlg, LineEnding, +}; + +const PKCS1_HEADER: &str = "-----BEGIN RSA PRIVATE KEY-----"; +const PKCS8_UNENCRYPTED_HEADER: &str = "-----BEGIN PRIVATE KEY-----"; +const PKCS8_ENCRYPTED_HEADER: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; +const OPENSSH_HEADER: &str = "-----BEGIN OPENSSH PRIVATE KEY-----"; + +pub const RSA_PKCS8_ALGORITHM_OID: ObjectIdentifier = + ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1"); + +#[derive(Debug)] +enum KeyType { + Ed25519, + Rsa, + Unknown, +} + +pub fn import_key( + encoded_key: String, + password: String, +) -> Result { + match encoded_key.lines().next() { + Some(PKCS1_HEADER) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::UnsupportedKeyType, + ssh_key: None, + }); + } + Some(PKCS8_UNENCRYPTED_HEADER) => { + return match import_pkcs8_key(encoded_key, None) { + Ok(result) => Ok(result), + Err(_) => Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }), + }; + } + Some(PKCS8_ENCRYPTED_HEADER) => match import_pkcs8_key(encoded_key, Some(password)) { + Ok(result) => { + return Ok(result); + } + Err(err) => match err { + SshKeyImportError::PasswordRequired => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::PasswordRequired, + ssh_key: None, + }); + } + SshKeyImportError::WrongPassword => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::WrongPassword, + ssh_key: None, + }); + } + SshKeyImportError::ParsingError => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + }, + }, + Some(OPENSSH_HEADER) => { + return import_openssh_key(encoded_key, password); + } + Some(_) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + None => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + } +} + +fn import_pkcs8_key( + encoded_key: String, + password: Option, +) -> Result { + let der = match SecretDocument::from_pem(&encoded_key) { + Ok((_, doc)) => doc, + Err(_) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + }; + + let decrypted_der = match password.clone() { + Some(password) => { + let encrypted_private_key_info = match EncryptedPrivateKeyInfo::from_der(der.as_bytes()) + { + Ok(info) => info, + Err(_) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + }; + match encrypted_private_key_info.decrypt(password.as_bytes()) { + Ok(der) => der, + Err(_) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::WrongPassword, + ssh_key: None, + }); + } + } + } + None => der, + }; + + let key_type: KeyType = match PrivateKeyInfo::from_der(decrypted_der.as_bytes()) + .map_err(|_| SshKeyImportError::ParsingError)? + .algorithm + .oid + { + ed25519::pkcs8::ALGORITHM_OID => KeyType::Ed25519, + RSA_PKCS8_ALGORITHM_OID => KeyType::Rsa, + _ => KeyType::Unknown, + }; + + match key_type { + KeyType::Ed25519 => { + let pk: ed25519::KeypairBytes = match password { + Some(password) => { + pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password) + .map_err(|err| match err { + ed25519::pkcs8::Error::EncryptedPrivateKey(_) => { + SshKeyImportError::WrongPassword + } + _ => SshKeyImportError::ParsingError, + })? + } + None => ed25519::pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key) + .map_err(|_| SshKeyImportError::ParsingError)?, + }; + let pk: Ed25519Keypair = + Ed25519Keypair::from(Ed25519PrivateKey::from_bytes(&pk.secret_key)); + let private_key = ssh_key::private::PrivateKey::from(pk); + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::Success, + ssh_key: Some(SshKey { + private_key: private_key.to_openssh(LineEnding::LF).unwrap().to_string(), + public_key: private_key.public_key().to_string(), + key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(), + }), + }); + } + KeyType::Rsa => { + let pk: rsa::RsaPrivateKey = match password { + Some(password) => { + pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password) + .map_err(|err| match err { + pkcs8::Error::EncryptedPrivateKey(_) => { + SshKeyImportError::WrongPassword + } + _ => SshKeyImportError::ParsingError, + })? + } + None => pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key) + .map_err(|_| SshKeyImportError::ParsingError)?, + }; + let rsa_keypair: Result = RsaKeypair::try_from(pk); + match rsa_keypair { + Ok(rsa_keypair) => { + let private_key = ssh_key::private::PrivateKey::from(rsa_keypair); + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::Success, + ssh_key: Some(SshKey { + private_key: private_key + .to_openssh(LineEnding::LF) + .unwrap() + .to_string(), + public_key: private_key.public_key().to_string(), + key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(), + }), + }); + } + Err(_) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + } + } + _ => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::UnsupportedKeyType, + ssh_key: None, + }); + } + } +} + +fn import_openssh_key( + encoded_key: String, + password: String, +) -> Result { + let private_key = ssh_key::private::PrivateKey::from_openssh(&encoded_key); + let private_key = match private_key { + Ok(k) => k, + Err(err) => { + match err { + ssh_key::Error::AlgorithmUnknown + | ssh_key::Error::AlgorithmUnsupported { algorithm: _ } => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::UnsupportedKeyType, + ssh_key: None, + }); + } + _ => {} + } + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + }; + + if private_key.is_encrypted() && password.is_empty() { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::PasswordRequired, + ssh_key: None, + }); + } + let private_key = if private_key.is_encrypted() { + match private_key.decrypt(password.as_bytes()) { + Ok(k) => k, + Err(_) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::WrongPassword, + ssh_key: None, + }); + } + } + } else { + private_key + }; + + match private_key.to_openssh(LineEnding::LF) { + Ok(private_key_openssh) => Ok(SshKeyImportResult { + status: SshKeyImportStatus::Success, + ssh_key: Some(SshKey { + private_key: private_key_openssh.to_string(), + public_key: private_key.public_key().to_string(), + key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(), + }), + }), + Err(_) => Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }), + } +} + +#[derive(PartialEq, Debug)] +pub enum SshKeyImportStatus { + /// ssh key was parsed correctly and will be returned in the result + Success, + /// ssh key was parsed correctly but is encrypted and requires a password + PasswordRequired, + /// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect + WrongPassword, + /// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key + ParsingError, + /// ssh key type is not supported + UnsupportedKeyType, +} + +pub enum SshKeyImportError { + ParsingError, + PasswordRequired, + WrongPassword, +} + +pub struct SshKeyImportResult { + pub status: SshKeyImportStatus, + pub ssh_key: Option, +} + +pub struct SshKey { + pub private_key: String, + pub public_key: String, + pub key_fingerprint: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn import_key_ed25519_openssh_unencrypted() { + let private_key = include_str!("./test_keys/ed25519_openssh_unencrypted"); + let public_key = include_str!("./test_keys/ed25519_openssh_unencrypted.pub").trim(); + let result = import_key(private_key.to_string(), "".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_ed25519_openssh_encrypted() { + let private_key = include_str!("./test_keys/ed25519_openssh_encrypted"); + let public_key = include_str!("./test_keys/ed25519_openssh_encrypted.pub").trim(); + let result = import_key(private_key.to_string(), "password".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_rsa_openssh_unencrypted() { + let private_key = include_str!("./test_keys/rsa_openssh_unencrypted"); + let public_key = include_str!("./test_keys/rsa_openssh_unencrypted.pub").trim(); + let result = import_key(private_key.to_string(), "".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_rsa_openssh_encrypted() { + let private_key = include_str!("./test_keys/rsa_openssh_encrypted"); + let public_key = include_str!("./test_keys/rsa_openssh_encrypted.pub").trim(); + let result = import_key(private_key.to_string(), "password".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_ed25519_pkcs8_unencrypted() { + let private_key = include_str!("./test_keys/ed25519_pkcs8_unencrypted"); + let public_key = + include_str!("./test_keys/ed25519_pkcs8_unencrypted.pub").replace("testkey", ""); + let public_key = public_key.trim(); + let result = import_key(private_key.to_string(), "".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_rsa_pkcs8_unencrypted() { + let private_key = include_str!("./test_keys/rsa_pkcs8_unencrypted"); + // for whatever reason pkcs8 + rsa does not include the comment in the public key + let public_key = + include_str!("./test_keys/rsa_pkcs8_unencrypted.pub").replace("testkey", ""); + let public_key = public_key.trim(); + let result = import_key(private_key.to_string(), "".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_rsa_pkcs8_encrypted() { + let private_key = include_str!("./test_keys/rsa_pkcs8_encrypted"); + let public_key = include_str!("./test_keys/rsa_pkcs8_encrypted.pub").replace("testkey", ""); + let public_key = public_key.trim(); + let result = import_key(private_key.to_string(), "password".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_ed25519_openssh_encrypted_wrong_password() { + let private_key = include_str!("./test_keys/ed25519_openssh_encrypted"); + let result = import_key(private_key.to_string(), "wrongpassword".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::WrongPassword); + } + + #[test] + fn import_non_key_error() { + let result = import_key("not a key".to_string(), "".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::ParsingError); + } + + #[test] + fn import_ecdsa_error() { + let private_key = include_str!("./test_keys/ecdsa_openssh_unencrypted"); + let result = import_key(private_key.to_string(), "".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::UnsupportedKeyType); + } +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs new file mode 100644 index 00000000000..ad0ac837afc --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs @@ -0,0 +1,118 @@ +use std::sync::Arc; + +use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; + +use bitwarden_russh::ssh_agent::{self, Key}; + +#[cfg_attr(target_os = "windows", path = "windows.rs")] +#[cfg_attr(target_os = "macos", path = "unix.rs")] +#[cfg_attr(target_os = "linux", path = "unix.rs")] +mod platform_ssh_agent; + +pub mod generator; +pub mod importer; + +#[derive(Clone)] +pub struct BitwardenDesktopAgent { + keystore: ssh_agent::KeyStore, + cancellation_token: CancellationToken, + show_ui_request_tx: tokio::sync::mpsc::Sender<(u32, String)>, + get_ui_response_rx: Arc>>, + request_id: Arc>, +} + +impl BitwardenDesktopAgent { + async fn get_request_id(&self) -> u32 { + let mut request_id = self.request_id.lock().await; + *request_id += 1; + *request_id + } +} + +impl ssh_agent::Agent for BitwardenDesktopAgent { + async fn confirm(&self, ssh_key: Key) -> bool { + let request_id = self.get_request_id().await; + + let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe(); + self.show_ui_request_tx + .send((request_id, ssh_key.cipher_uuid.clone())) + .await + .expect("Should send request to ui"); + while let Ok((id, response)) = rx_channel.recv().await { + if id == request_id { + return response; + } + } + false + } +} + +impl BitwardenDesktopAgent { + pub fn stop(&self) { + self.cancellation_token.cancel(); + self.keystore + .0 + .write() + .expect("RwLock is not poisoned") + .clear(); + } + + pub fn set_keys( + &mut self, + new_keys: Vec<(String, String, String)>, + ) -> Result<(), anyhow::Error> { + let keystore = &mut self.keystore; + keystore.0.write().expect("RwLock is not poisoned").clear(); + + for (key, name, cipher_id) in new_keys.iter() { + match parse_key_safe(&key) { + Ok(private_key) => { + let public_key_bytes = private_key + .public_key() + .to_bytes() + .expect("Cipher private key is always correctly parsed"); + keystore.0.write().expect("RwLock is not poisoned").insert( + public_key_bytes, + Key { + private_key: Some(private_key), + name: name.clone(), + cipher_uuid: cipher_id.clone(), + }, + ); + } + Err(e) => { + eprintln!("[SSH Agent Native Module] Error while parsing key: {}", e); + } + } + } + + Ok(()) + } + + pub fn lock(&mut self) -> Result<(), anyhow::Error> { + let keystore = &mut self.keystore; + keystore + .0 + .write() + .expect("RwLock is not poisoned") + .iter_mut() + .for_each(|(_public_key, key)| { + key.private_key = None; + }); + Ok(()) + } +} + +fn parse_key_safe(pem: &str) -> Result { + match ssh_key::private::PrivateKey::from_openssh(pem) { + Ok(key) => match key.public_key().to_bytes() { + Ok(_) => Ok(key), + Err(e) => Err(anyhow::Error::msg(format!( + "Failed to parse public key: {}", + e + ))), + }, + Err(e) => Err(anyhow::Error::msg(format!("Failed to parse key: {}", e))), + } +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs new file mode 100644 index 00000000000..69399ae7530 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs @@ -0,0 +1,60 @@ +use std::{ + io, + pin::Pin, + task::{Context, Poll}, +}; + +use futures::Stream; +use tokio::{ + net::windows::named_pipe::{NamedPipeServer, ServerOptions}, + select, +}; +use tokio_util::sync::CancellationToken; + +const PIPE_NAME: &str = r"\\.\pipe\openssh-ssh-agent"; + +#[pin_project::pin_project] +pub struct NamedPipeServerStream { + rx: tokio::sync::mpsc::Receiver, +} + +impl NamedPipeServerStream { + pub fn new(cancellation_token: CancellationToken) -> Self { + let (tx, rx) = tokio::sync::mpsc::channel(16); + tokio::spawn(async move { + println!( + "[SSH Agent Native Module] Creating named pipe server on {}", + PIPE_NAME + ); + let mut listener = ServerOptions::new().create(PIPE_NAME).unwrap(); + loop { + println!("[SSH Agent Native Module] Waiting for connection"); + select! { + _ = cancellation_token.cancelled() => { + println!("[SSH Agent Native Module] Cancellation token triggered, stopping named pipe server"); + break; + } + _ = listener.connect() => { + println!("[SSH Agent Native Module] Incoming connection"); + tx.send(listener).await.unwrap(); + listener = ServerOptions::new().create(PIPE_NAME).unwrap(); + } + } + } + }); + Self { rx } + } +} + +impl Stream for NamedPipeServerStream { + type Item = io::Result; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let this = self.project(); + + this.rx.poll_recv(cx).map(|v| v.map(Ok)) + } +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted new file mode 100644 index 00000000000..9cf518f8af7 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRQzzQ8nQEouF1FMSHkPx1nejNCzF7g +Yb8MHXLdBFM0uJkWs0vzgLJkttts2eDv3SHJqIH6qHpkLtEvgMXE5WcaAAAAoOO1BebjtQ +XmAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFDPNDydASi4XUUx +IeQ/HWd6M0LMXuBhvwwdct0EUzS4mRazS/OAsmS222zZ4O/dIcmogfqoemQu0S+AxcTlZx +oAAAAhAKnIXk6H0Hs3HblklaZ6UmEjjdE/0t7EdYixpMmtpJ4eAAAAB3Rlc3RrZXk= +-----END OPENSSH PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted.pub new file mode 100644 index 00000000000..75e08b88b2f --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFDPNDydASi4XUUxIeQ/HWd6M0LMXuBhvwwdct0EUzS4mRazS/OAsmS222zZ4O/dIcmogfqoemQu0S+AxcTlZxo= testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted new file mode 100644 index 00000000000..d3244a3d945 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAUTNb0if +fqsoqtfv70CfukAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHGs3Uw3eyqnFjBI +2eb7Qto4KVc34ZdnBac59Bab54BLAAAAkPA6aovfxQbP6FoOfaRH6u22CxqiUM0bbMpuFf +WETn9FLaBE6LjoHH0ZI5rzNjJaQUNfx0cRcqsIrexw8YINrdVjySmEqrl5hw8gpgy0gGP5 +1Y6vKWdHdrxJCA9YMFOfDs0UhPfpLKZCwm2Sg+Bd8arlI8Gy7y4Jj/60v2bZOLhD2IZQnK +NdJ8xATiIINuTy4g== +-----END OPENSSH PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted.pub new file mode 100644 index 00000000000..1188fa43f1e --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHGs3Uw3eyqnFjBI2eb7Qto4KVc34ZdnBac59Bab54BL testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted new file mode 100644 index 00000000000..08184f3184e --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAyQo22TXXNqvF+L8jUSSNeu8UqrsDjvf9pwIwDC9ML6gAAAJDSHpL60h6S ++gAAAAtzc2gtZWQyNTUxOQAAACAyQo22TXXNqvF+L8jUSSNeu8UqrsDjvf9pwIwDC9ML6g +AAAECLdlFLIJbEiFo/f0ROdXMNZAPHGPNhvbbftaPsUZEjaDJCjbZNdc2q8X4vyNRJI167 +xSquwOO9/2nAjAML0wvqAAAAB3Rlc3RrZXkBAgMEBQY= +-----END OPENSSH PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted.pub new file mode 100644 index 00000000000..5c398822022 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDJCjbZNdc2q8X4vyNRJI167xSquwOO9/2nAjAML0wvq testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted new file mode 100644 index 00000000000..09eb728601e --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MFECAQEwBQYDK2VwBCIEIDY6/OAdDr3PbDss9NsLXK4CxiKUvz5/R9uvjtIzj4Sz +gSEAxsxm1xpZ/4lKIRYm0JrJ5gRZUh7H24/YT/0qGVGzPa0= +-----END PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted.pub new file mode 100644 index 00000000000..40997e18c89 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMbMZtcaWf+JSiEWJtCayeYEWVIex9uP2E/9KhlRsz2t diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted new file mode 100644 index 00000000000..bb7bbd85cf9 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted @@ -0,0 +1,39 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABApatKZWf +0kXnaSVhty/RaKAAAAGAAAAAEAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQC/v18xGP3q +zRV9iWqyiuwHZ4GpC4K2NO2/i2Yv5A3/bnal7CmiMh/S78lphgxcWtFkwrwlb321FmdHBv +6KOW+EzSiPvmsdkkbpfBXB3Qf2SlhZOZZ7lYeu8KAxL3exvvn8O1GGlUjXGUrFgmC60tHW +DBc1Ncmo8a2dwDLmA/sbLa8su2dvYEFmRg1vaytLDpkn8GS7zAxrUl/g0W2RwkPsByduUz +iQuX90v9WAy7MqOlwBRq6t5o8wdDBVODe0VIXC7N1OS42YUsKF+N0XOnLiJrIIKkXpahMD +pKZHeHQAdUQzsJVhKoLJR8DNDTYyhnJoQG7Q6m2gDTca9oAWvsBiNoEwCvwrt7cDNCz/Gs +lH9HXQgfWcVXn8+fuZgvjO3CxUI16Ev33m0jWoOKJcgK/ZLRnk8SEvsJ8NO32MeR/qUb7I +N/yUcDmPMI/3ecQsakF2cwNzHkyiGVo//yVTpf+vk8b89L+GXbYU5rtswtc2ZEGsQnUkao +NqS8mHqhWQBUkAAAWArmugDAR1KlxY8c/esWbgQ4oP/pAQApehDcFYOrS9Zo78Os4ofEd1 +HkgM7VG1IJafCnn+q+2VXD645zCsx5UM5Y7TcjYDp7reM19Z9JCidSVilleRedTj6LTZx1 +SvetIrTfr81SP6ZGZxNiM0AfIZJO5vk+NliDdbUibvAuLp3oYbzMS3syuRkJePWu+KSxym +nm2+88Wku94p6SIfGRT3nQsMfLS9x6fGQP5Z71DM91V33WCVhrBnvHgNxuAzHDZNfzbPu9 +f2ZD1JGh8azDPe0XRD2jZTyd3Nt+uFMcwnMdigTXaTHExEFkTdQBea1YoprIG56iNZTSoU +/RwE4A0gdrSgJnh+6p8w05u+ia0N2WSL5ZT9QydPhwB8pGHuGBYoXFcAcFwCnIAExPtIUh +wLx1NfC/B2MuD3Uwbx96q5a7xMTH51v0eQDdY3mQzdq/8OHHn9vzmEfV6mxmuyoa0Vh+WG +l2WLB2vD5w0JwRAFx6a3m/rD7iQLDvK3UiYJ7DVz5G3/1w2m4QbXIPCfI3XHU12Pye2a0m +/+/wkS4/BchqB0T4PJm6xfEynXwkEolndf+EvuLSf53XSJ2tfeFPGmmCyPoy9JxCce7wVk +FB/SJw6LXSGUO0QA6vzxbzLEMNrqrpcCiUvDGTA6jds0HnSl8hhgMuZOtQDbFoovIHX0kl +I5pD5pqaUNvQ3+RDFV3qdZyDntaPwCNJumfqUy46GAhYVN2O4p0HxDTs4/c2rkv+fGnG/P +8wc7ACz3QNdjb7XMrW3/vNuwrh/sIjNYM2aiVWtRNPU8bbSmc1sYtpJZ5CsWK1TNrDrY6R +OV89NjBoEC5OXb1c75VdN/jSssvn72XIHjkkDEPboDfmPe889VHfsVoBm18uvWPB4lffdm +4yXAr+Cx16HeiINjcy6iKym2p4ED5IGaSXlmw/6fFgyh2iF7kZTnHawVPTqJNBVMaBRvHn +ylMBLhhEkrXqW43P4uD6l0gWCAPBczcSjHv3Yo28ExtI0QKNk/Uwd2q2kxFRWCtqUyQkrF +KG9IK+ixqstMo+xEb+jcCxCswpJitEIrDOXd51sd7PjCGZtAQ6ycpOuFfCIhwxlBUZdf2O +kM/oKqN/MKMDk+H/OVl8XrLalBOXYDllW+NsL8W6F8DMcdurpQ8lCJHHWBgOdNd62STdvZ +LBf7v8OIrC6F0bVGushsxb7cwGiUrjqUfWjhZoKx35V0dWBcGx7GvzARkvSUM22q14lc7+ +XTP0qC8tcRQfRbnBPJdmnbPDrJeJcDv2ZdbAPdzf2C7cLuuP3mNwLCrLUc7gcF/xgH+Xtd +6KOvzt2UuWv5+cqWOsNspG+lCY0P11BPhlMvmZKO8RGVGg7PKAatG4mSH4IgO4DN2t7U9B +j+v2jq2z5O8O4yJ8T2kWnBlhWzlBoL+R6aaat421f0v+tW/kEAouBQob5I0u1VLB2FkpZE +6tOCK47iuarhf/86NtlPfCM9PdWJQOKcYQ8DCQhp5Lvgd0Vj3WzY+BISDdB2omGRhLUly/ +i40YPASAVnWvgqpCQ4E3rs4DWI/kEcvQH8zVq2YoRa6fVrVf1w/GLFC7m/wkxw8fDfZgMS +Mu+ygbFa9H3aOSZMpTXhdssbOhU70fZOe6GWY9kLBNV4trQeb/pRdbEbMtEmN5TLESgwLA +43dVdHjvpZS677FN/d9+q+pr0Xnuc2VdlXkUyOyv1lFPJIN/XIotiDTnZ3epQQ1zQ3mx32 +8Op2EVgFWpwNmGXJ1zCCA6loUG7e4W/iXkKQxTvOM0fmE4a1Y387GDwJ+pZevYOIOYTkTa +l5jM/6Wm3pLNyE8Ynw3OX0T/p9TO1i3DlXXE/LzcWJFFXAQMo+kc+GlXqjP7K7c6xjQ6vx +2MmKBw== +-----END OPENSSH PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted.pub new file mode 100644 index 00000000000..d37f573b686 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/v18xGP3qzRV9iWqyiuwHZ4GpC4K2NO2/i2Yv5A3/bnal7CmiMh/S78lphgxcWtFkwrwlb321FmdHBv6KOW+EzSiPvmsdkkbpfBXB3Qf2SlhZOZZ7lYeu8KAxL3exvvn8O1GGlUjXGUrFgmC60tHWDBc1Ncmo8a2dwDLmA/sbLa8su2dvYEFmRg1vaytLDpkn8GS7zAxrUl/g0W2RwkPsByduUziQuX90v9WAy7MqOlwBRq6t5o8wdDBVODe0VIXC7N1OS42YUsKF+N0XOnLiJrIIKkXpahMDpKZHeHQAdUQzsJVhKoLJR8DNDTYyhnJoQG7Q6m2gDTca9oAWvsBiNoEwCvwrt7cDNCz/GslH9HXQgfWcVXn8+fuZgvjO3CxUI16Ev33m0jWoOKJcgK/ZLRnk8SEvsJ8NO32MeR/qUb7IN/yUcDmPMI/3ecQsakF2cwNzHkyiGVo//yVTpf+vk8b89L+GXbYU5rtswtc2ZEGsQnUkaoNqS8mHqhWQBUk= testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted new file mode 100644 index 00000000000..0d2692e14a2 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAYEAtVIe0gnPtD6299/roT7ntZgVe+qIqIMIruJdI2xTanLGhNpBOlzg +WqokbQK+aXATcaB7iQL1SPxIWV2M4jEBQbZuimIgDQvKbJ4TZPKEe1VdsrfuIo+9pDK7cG +Kc+JiWhKjqeTRMj91/qR1fW5IWOUyE1rkwhTNkwJqtYKZLVmd4TXtQsYMMC+I0cz4krfk1 +Yqmaae/gj12h8BvE3Y+Koof4JoLsqPufH+H/bVEayv63RyAQ1/tUv9l+rwJ+svWV4X3zf3 +z40hGF43L/NGl90Vutbn7b9G/RgEdiXyLZciP3XbWbLUM+r7mG9KNuSeoixe5jok15UKqC +XXxVb5IEZ73kaubSfz9JtsqtKG/OjOq6Fbl3Ky7kjvJyGpIvesuSInlpzPXqbLUCLJJfOA +PUZ1wi8uuuRNePzQBMMhq8UtAbB2Dy16d+HlgghzQ00NxtbQMfDZBdApfxm3shIxkUcHzb +DSvriHVaGGoOkmHPAmsdMsMiekuUMe9ljdOhmdTxAAAFgF8XjBxfF4wcAAAAB3NzaC1yc2 +EAAAGBALVSHtIJz7Q+tvff66E+57WYFXvqiKiDCK7iXSNsU2pyxoTaQTpc4FqqJG0Cvmlw +E3Gge4kC9Uj8SFldjOIxAUG2bopiIA0LymyeE2TyhHtVXbK37iKPvaQyu3BinPiYloSo6n +k0TI/df6kdX1uSFjlMhNa5MIUzZMCarWCmS1ZneE17ULGDDAviNHM+JK35NWKpmmnv4I9d +ofAbxN2PiqKH+CaC7Kj7nx/h/21RGsr+t0cgENf7VL/Zfq8CfrL1leF98398+NIRheNy/z +RpfdFbrW5+2/Rv0YBHYl8i2XIj9121my1DPq+5hvSjbknqIsXuY6JNeVCqgl18VW+SBGe9 +5Grm0n8/SbbKrShvzozquhW5dysu5I7ychqSL3rLkiJ5acz16my1AiySXzgD1GdcIvLrrk +TXj80ATDIavFLQGwdg8tenfh5YIIc0NNDcbW0DHw2QXQKX8Zt7ISMZFHB82w0r64h1Whhq +DpJhzwJrHTLDInpLlDHvZY3ToZnU8QAAAAMBAAEAAAGAEL3wpRWtVTf+NnR5QgX4KJsOjs +bI0ABrVpSFo43uxNMss9sgLzagq5ZurxcUBFHKJdF63puEkPTkbEX4SnFaa5of6kylp3a5 +fd55rXY8F9Q5xtT3Wr8ZdFYP2xBr7INQUJb1MXRMBnOeBDw3UBH01d0UHexzB7WHXcZacG +Ria+u5XrQebwmJ3PYJwENSaTLrxDyjSplQy4QKfgxeWNPWaevylIG9vtue5Xd9WXdl6Szs +ONfD3mFxQZagPSIWl0kYIjS3P2ZpLe8+sakRcfci8RjEUP7U+QxqY5VaQScjyX1cSYeQLz +t+/6Tb167aNtQ8CVW3IzM2EEN1BrSbVxFkxWFLxogAHct06Kn87nPn2+PWGWOVCBp9KheO +FszWAJ0Kzjmaga2BpOJcrwjSpGopAb1YPIoRPVepVZlQ4gGwy5gXCFwykT9WTBoJfg0BMQ +r3MSNcoc97eBomIWEa34K0FuQ3rVjMv9ylfyLvDBbRqTJ5zebeOuU+yCQHZUKk8klRAAAA +wAsToNZvYWRsOMTWQom0EW1IHzoL8Cyua+uh72zZi/7enm4yHPJiu2KNgQXfB0GEEjHjbo +9peCW3gZGTV+Ee+cAqwYLlt0SMl/VJNxN3rEG7BAqPZb42Ii2XGjaxzFq0cliUGAdo6UEd +swU8d2I7m9vIZm4nDXzsWOBWgonTKBNyL0DQ6KNOGEyj8W0BTCm7Rzwy7EKzFWbIxr4lSc +vDrJ3t6kOd7jZTF58kRMT0nxR0bf43YzF/3/qSvLYhQm/OOAAAAMEA2F6Yp8SrpQDNDFxh +gi4GeywArrQO9r3EHjnBZi/bacxllSzCGXAvp7m9OKC1VD2wQP2JL1VEIZRUTuGGT6itrm +QpX8OgoxlEJrlC5W0kHumZ3MFGd33W11u37gOilmd6+VfVXBziNG2rFohweAgs8X+Sg5AA +nIfMV6ySXUlvLzMHpGeKRRnnQq9Cwn4rDkVQENLd1i4e2nWFhaPTUwVMR8YuOT766bywr3 +7vG1PQLF7hnf2c/oPHAru+XD9gJWs5AAAAwQDWiB2G23F4Tvq8FiK2mMusSjQzHupl83rm +o3BSNRCvCjaLx6bWhDPSA1edNEF7VuP6rSp+i+UfSORHwOnlgnrvtcJeoDuA72hUeYuqD/ +1C9gghdhKzGTVf/IGTX1tH3rn2Gq9TEyrJs/ITcoOyZprz7VbaD3bP/NEER+m1EHi2TS/3 +SXQEtRm+IIBwba+QLUcsrWdQyIO+1OCXywDrAw50s7tjgr/goHgXTcrSXaKcIEOlPgBZH3 +YPuVuEtRYgX3kAAAAHdGVzdGtleQECAwQ= +-----END OPENSSH PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted.pub new file mode 100644 index 00000000000..9ec8fec5c58 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1Uh7SCc+0Prb33+uhPue1mBV76oiogwiu4l0jbFNqcsaE2kE6XOBaqiRtAr5pcBNxoHuJAvVI/EhZXYziMQFBtm6KYiANC8psnhNk8oR7VV2yt+4ij72kMrtwYpz4mJaEqOp5NEyP3X+pHV9bkhY5TITWuTCFM2TAmq1gpktWZ3hNe1CxgwwL4jRzPiSt+TViqZpp7+CPXaHwG8Tdj4qih/gmguyo+58f4f9tURrK/rdHIBDX+1S/2X6vAn6y9ZXhffN/fPjSEYXjcv80aX3RW61uftv0b9GAR2JfItlyI/ddtZstQz6vuYb0o25J6iLF7mOiTXlQqoJdfFVvkgRnveRq5tJ/P0m2yq0ob86M6roVuXcrLuSO8nIaki96y5IieWnM9epstQIskl84A9RnXCLy665E14/NAEwyGrxS0BsHYPLXp34eWCCHNDTQ3G1tAx8NkF0Cl/GbeyEjGRRwfNsNK+uIdVoYag6SYc8Cax0ywyJ6S5Qx72WN06GZ1PE= testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted new file mode 100644 index 00000000000..e84d1f07a31 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted @@ -0,0 +1,42 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIHdTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQXquAya5XFx11QEPm +KCSnlwICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEAQIEEKVtEIkI2ELppfUQ +IwfNzowEggcQtWhXVz3LunYTSRVgnexcHEaGkUF6l6a0mGaLSczl+jdCwbbBxibU +EvN7+WMQ44shOk3LyThg0Irl22/7FuovmYc3TSeoMQH4mTROKF+9793v0UMAIAYd +ZhTsexTGncCOt//bq6Fl+L+qPNEkY/OjS+wI9MbOn/Agbcr8/IFSOxuSixxoTKgq +4QR5Ra3USCLyfm+3BoGPMk3tbEjrwjvzx/eTaWzt6hdc0yX4ehtqExF8WAYB43DW +3Y1slA1T464/f1j4KXhoEXDTBOuvNvnbr7lhap8LERIGYGnQKv2m2Kw57Wultnoe +joEQ+vTl5n92HI77H8tbgSbTYuEQ2n9pDD7AAzYGBn15c4dYEEGJYdHnqfkEF+6F +EgPa+Xhj2qqk5nd1bzPSv6iX7XfAX2sRzfZfoaFETmR0ZKbs0aMsndC5wVvd3LpA +m86VUihQxDvU8F4gizrNYj4NaNRv4lrxBj7Kb6BO/qT3DB8Uqu43oyrvA90iMigi +EvuCViwwhwCpe+AxCqLGrzvIpiZCksTOtSPEvnMehw2WA3yd/n88Nis5zD4b65+q +Tx9Q0Qm1LIi1Bq+s60+W1HK3KfaLrJaoX3JARZoWfxurZwtj+cMlo5zK1Ha2HHqQ +kVn21tOcQU/Yljt3Db+CKZ5Tos/rPywxGnkeMABzJgyajPHkYaSgWZrOEueihfS1 +5eDtEMBehEyHfcUrL7XGnn4lOzwQHZIEFnVdV0YGaQY8Wz212IjeWxV09gM2OEP6 +PEDI3GSsqOnGkPrnson5tsIUcvpk9smy9AA9qVhNowzeWCWmsF8K9fn/O94tIzyN +2EK0tkf8oDVROlbEh/jDa2aAHqPGCXBEqq1CbZXQpNk4FlRzkjtxdzPNiXLf45xO +IjOTTzgaVYWiKZD9ymNjNPIaDCPB6c4LtUm86xUQzXdztBm1AOI3PrNI6nIHxWbF +bPeEkJMRiN7C9j5nQMgQRB67CeLhzvqUdyfrYhzc7HY479sKDt9Qn8R0wpFw0QSA +G1gpGyxFaBFSdIsil5K4IZYXxh7qTlOKzaqArTI0Dnuk8Y67z8zaxN5BkvOfBd+Q +SoDz6dzn7KIJrK4XP3IoNfs6EVT/tlMPRY3Y/Ug+5YYjRE497cMxW8jdf3ZwgWHQ +JubPH+0IpwNNZOOf4JXALULsDj0N7rJ1iZAY67b+7YMin3Pz0AGQhQdEdqnhaxPh +oMvL9xFewkyujwCmPj1oQi1Uj2tc1i4ZpxY0XmYn/FQiQH9/XLdIlOMSTwGx86bw +90e9VJHfCmflLOpENvv5xr2isNbn0aXNAOQ4drWJaYLselW2Y4N1iqBCWJKFyDGw +4DevhhamEvsrdoKgvnuzfvA44kQGmfTjCuMu7IR5zkxevONNrynKcHkoWATzgxSS +leXCxzc9VA0W7XUSMypHGPNHJCwYZvSWGx0qGI3VREUk2J7OeVjXCFNeHFc2Le3P +dAm+DqRiyPBVX+yW+i7rjZLyypLzmYo9CyhlohOxTeGa6iTxBUZfYGoc0eJNqfgN +/5hkoPFYGkcd/p41SKSg7akrJPRc+uftH0oVI0wVorGSVOvwXRn7QM+wFKlv3DQD +ysMP7cOKqMyhJsqeW74/iWEmhbFIDKexSd/KTQ6PirVlzj7148Fl++yxaZpnZ6MY +iyzifvLcT701GaewIwi9YR9f1BWUXYHTjK3sB3lLPyMbA4w9bRkylcKrbGf85q0E +LXPlfh+1C9JctczDCqr2iLRoc/5j23GeN8RWfUNpZuxjFv9sxkV4iG+UapIuOBIc +Os4//3w24XcTXYqBdX2Y7+238xq6/94+4hIhXAcMFc2Nr3CEAZCuKYChVL9CSA3v +4sZM4rbOz6kWTC2G3SAtkLSk7hCJ6HLXzrnDb4++g3JYJWLeaQ+4ZaxWuKymnehN +xumXCwCn0stmCjXYV/yM3TeVnMfBTIB13KAjbn0czGW00nj79rNJJzkOlp9tIPen +pUPRFPWjgLF+hVQrwqJ3HPmt6Rt6mKzZ4FEpBXMDjvlKabnFvBdl3gbNHSfxhGHi +FzG3phg1CiXaURQUAf21PV+djfBha7kDwMXnpgZ+PIyGDxRj61StV/NSlhg+8GrL +ccoDOkfpy2zn++rmAqA21rTEChFN5djdsJw45GqPKUPOAgxKBsvqpoMIqq/C2pHP +iMiBriZULV9l0tHn5MMcNQbYAmp4BsTo6maHByAVm1/7/VPQn6EieuGroYgSk2H7 +pnwM01IUfGGP3NKlq9EiiF1gz8acZ5v8+jkZM2pIzh8Trw0mtwBpnyiyXmpbR/RG +m/TTU/gNQ/94ZaNJ/shPoBwikWXvOm+0Z0ZAwu3xefTyENGhjmb5GXshEN/5WwCm +NNrtUPlkGkYJrnSCVM/lHtjShwbLw2w/1sag1uDuXwirxxYh9r7D6HQ= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted.pub new file mode 100644 index 00000000000..f3c1b15f0a3 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCcHkc0xfH4w9aW41S9M/BfancSY4QPc2O4G1cRjFfK8QrLEGDA7NiHtoEML0afcurRXD3NVxuKaAns0w6EoS4CjzXUqVHTLA4SUyuapr8k0Eu2xOpbCwC3jDovhckoKloq7BvE6rC2i5wjSMadtIJKt/dqWI3HLjUMz1BxQJAU/qAbicj1SFZSjA/MubVBzcq93XOvByMtlIFu7wami3FTc37rVkGeUFHtK8ZbvG3n1aaTF79bBgSPuoq5BfcMdGr4WfQyGQzgse4v4hQ8yKYrtE0jo0kf06hEORimwOIU/W5IH1r+/xFs7qGKcPnFSZRIFv5LfMPTo8b+OsBRflosyfUumDEX97GZE7DSQl0EJzNvWeKwl7dQ8RUJTkbph2CjrxY77DFim+165Uj/WRr4uq2qMNhA2xNSD19+TA6AHdpGw4WZd37q2/n+EddlaJEH8MzpgtHNG9MiYh5ScZ+AG0QugflozJcQNc7n8N9Lpu1sRoejV5RhurHg/TYwVK8= testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted new file mode 100644 index 00000000000..0bfe2bc5067 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted @@ -0,0 +1,40 @@ +-----BEGIN PRIVATE KEY----- +MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQCn4+QiJojZ9mgc +9KYJIvDWGaz4qFhf0CButg6L8zEoHKwuiN+mqcEciCCOa9BNiJmm8NTTehZvrrgl +GG59zIbqYtDAHjVn+vtb49xPzIv+M651Yqj08lIbR9tEIHKCq7aH8GlDm8NgG9Ez +JGjlL7okQym4TH1MHl+s4mUyr/qb2unlZBDixAQsphU8iCLftukWCIkmQg4CSj1G +h3WbBlZ+EX5eW0EXuAw4XsSbBTWV9CHRowVIpYqPvEYSpHsoCjEcd988p19hpiGk +nA0J4z7JfUlNgyT/1chb8GCTDT+2DCBRApbsIg6TOBVS+PR6emAQ3eZzUW0+3/oR +M4ip0ujltQy8uU6gvYIAqx5wXGMThVpZcUgahKiSsVo/s4b84iMe4DG3W8jz4qi6 +yyNv0VedEzPUZ1lXd1GJFoy9uKNuSTe+1ksicAcluZN6LuNsPHcPxFCzOcmoNnVX +EKAXInt+ys//5CDVasroZSAHZnDjUD4oNsLI3VIOnGxgXrkwSH0CAwEAAQKCAYAA +2SDMf7OBHw1OGM9OQa1ZS4u+ktfQHhn31+FxbrhWGp+lDt8gYABVf6Y4dKN6rMtn +7D9gVSAlZCAn3Hx8aWAvcXHaspxe9YXiZDTh+Kd8EIXxBQn+TiDA5LH0dryABqmM +p20vYKtR7OS3lIIXfFBSrBMwdunKzLwmKwZLWq0SWf6vVbwpxRyR9CyByodF6Djm +ZK3QB2qQ3jqlL1HWXL0VnyArY7HLvUvfLLK4vMPqnsSH+FdHvhcEhwqMlWT44g+f +hqWtCJNnjDgLK3FPbI8Pz9TF8dWJvOmp5Q6iSBua1e9x2LizVuNSqiFc7ZTLeoG4 +nDj7T2BtqB0E1rNUDEN1aBo+UZmHJK7LrzfW/B+ssi2WwIpfxYa1lO6HFod5/YQi +XV1GunyH1chCsbvOFtXvAHASO4HTKlJNbWhRF1GXqnKpAaHDPCVuwp3eq6Yf0oLb +XrL3KFZ3jwWiWbpQXRVvpqzaJwZn3CN1yQgYS9j17a9wrPky+BoJxXjZ/oImWLEC +gcEA0lkLwiHvmTYFTCC7PN938Agk9/NQs5PQ18MRn9OJmyfSpYqf/gNp+Md7xUgt +F/MTif7uelp2J7DYf6fj9EYf9g4EuW+SQgFP4pfiJn1+zGFeTQq1ISvwjsA4E8ZS +t+GIumjZTg6YiL1/A79u4wm24swt7iqnVViOPtPGOM34S1tAamjZzq2eZDmAF6pA +fmuTMdinCMR1E1kNJYbxeqLiqQCXuwBBnHOOOJofN3AkvzjRUBB9udvniqYxH3PQ +cxPxAoHBAMxT5KwBhZhnJedYN87Kkcpl7xdMkpU8b+aXeZoNykCeoC+wgIQexnSW +mFk4HPkCNxvCWlbkOT1MHrTAKFnaOww23Ob+Vi6A9n0rozo9vtoJig114GB0gUqE +mtfLhO1P5AE8yzogE+ILHyp0BqXt8vGIfzpDnCkN+GKl8gOOMPrR4NAcLO+Rshc5 +nLs7BGB4SEi126Y6mSfp85m0++1QhWMz9HzqJEHCWKVcZYdCdEONP9js04EUnK33 +KtlJIWzZTQKBwAT0pBpGwmZRp35Lpx2gBitZhcVxrg0NBnaO2fNyAGPvZD8SLQLH +AdAiov/a23Uc/PDbWLL5Pp9gwzj+s5glrssVOXdE8aUscr1b5rARdNNL1/Tos6u8 +ZUZ3sNqGaZx7a8U4gyYboexWyo9EC1C+AdkGBm7+AkM4euFwC9N6xsa/t5zKK5d6 +76hc0m+8SxivYCBkgkrqlfeGuZCQxU+mVsC0it6U+va8ojUjLGkZ80OuCwBf4xZl +3+acU7vx9o8/gQKBwB7BrhU6MWrsc+cr/1KQaXum9mNyckomi82RFYvb8Yrilcg3 +8FBy9XqNRKeBa9MLw1HZYpHbzsXsVF7u4eQMloDTLVNUC5L6dKAI1owoyTa24uH9 +0WWTg/a8mTZMe1jhgrew+AJq27NV6z4PswR9GenDmyshDDudz7rBsflZCQRoXUfW +RelV7BHU6UPBsXn4ASF4xnRyM6WvcKy9coKZcUqqgm3fLM/9OizCCMJgfXHBrE+x +7nBqst746qlEedSRrQKBwQCVYwwKCHNlZxl0/NMkDJ+hp7/InHF6mz/3VO58iCb1 +9TLDVUC2dDGPXNYwWTT9PclefwV5HNBHcAfTzgB4dpQyNiDyV914HL7DFEGduoPn +wBYjeFre54v0YjjnskjJO7myircdbdX//i+7LMUw5aZZXCC8a5BD/rdV6IKJWJG5 +QBXbe5fVf1XwOjBTzlhIPIqhNFfSu+mFikp5BRwHGBqsKMju6inYmW6YADeY/SvO +QjDEB37RqGZxqyIx8V2ZYwU= +-----END PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted.pub new file mode 100644 index 00000000000..a3e04eed461 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCn4+QiJojZ9mgc9KYJIvDWGaz4qFhf0CButg6L8zEoHKwuiN+mqcEciCCOa9BNiJmm8NTTehZvrrglGG59zIbqYtDAHjVn+vtb49xPzIv+M651Yqj08lIbR9tEIHKCq7aH8GlDm8NgG9EzJGjlL7okQym4TH1MHl+s4mUyr/qb2unlZBDixAQsphU8iCLftukWCIkmQg4CSj1Gh3WbBlZ+EX5eW0EXuAw4XsSbBTWV9CHRowVIpYqPvEYSpHsoCjEcd988p19hpiGknA0J4z7JfUlNgyT/1chb8GCTDT+2DCBRApbsIg6TOBVS+PR6emAQ3eZzUW0+3/oRM4ip0ujltQy8uU6gvYIAqx5wXGMThVpZcUgahKiSsVo/s4b84iMe4DG3W8jz4qi6yyNv0VedEzPUZ1lXd1GJFoy9uKNuSTe+1ksicAcluZN6LuNsPHcPxFCzOcmoNnVXEKAXInt+ys//5CDVasroZSAHZnDjUD4oNsLI3VIOnGxgXrkwSH0= testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs new file mode 100644 index 00000000000..c1a39506660 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs @@ -0,0 +1,77 @@ +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; + +use bitwarden_russh::ssh_agent; +use homedir::my_home; +use tokio::{net::UnixListener, sync::Mutex}; +use tokio_util::sync::CancellationToken; + +use super::BitwardenDesktopAgent; + +impl BitwardenDesktopAgent { + pub async fn start_server( + auth_request_tx: tokio::sync::mpsc::Sender<(u32, String)>, + auth_response_rx: Arc>>, + ) -> Result { + use std::path::PathBuf; + + let agent = BitwardenDesktopAgent { + keystore: ssh_agent::KeyStore(Arc::new(RwLock::new(HashMap::new()))), + cancellation_token: CancellationToken::new(), + show_ui_request_tx: auth_request_tx, + get_ui_response_rx: auth_response_rx, + request_id: Arc::new(tokio::sync::Mutex::new(0)), + }; + let cloned_agent_state = agent.clone(); + tokio::spawn(async move { + let ssh_path = match std::env::var("BITWARDEN_SSH_AUTH_SOCK") { + Ok(path) => path, + Err(_) => { + println!("[SSH Agent Native Module] BITWARDEN_SSH_AUTH_SOCK not set, using default path"); + + let ssh_agent_directory = match my_home() { + Ok(Some(home)) => home, + _ => PathBuf::from("/tmp/"), + }; + ssh_agent_directory + .join(".bitwarden-ssh-agent.sock") + .to_str() + .expect("Path should be valid") + .to_owned() + } + }; + + println!( + "[SSH Agent Native Module] Starting SSH Agent server on {:?}", + ssh_path + ); + let sockname = std::path::Path::new(&ssh_path); + let _ = std::fs::remove_file(sockname); + match UnixListener::bind(sockname) { + Ok(listener) => { + let wrapper = tokio_stream::wrappers::UnixListenerStream::new(listener); + let cloned_keystore = cloned_agent_state.keystore.clone(); + let cloned_cancellation_token = cloned_agent_state.cancellation_token.clone(); + let _ = ssh_agent::serve( + wrapper, + cloned_agent_state, + cloned_keystore, + cloned_cancellation_token, + ) + .await; + println!("[SSH Agent Native Module] SSH Agent server exited"); + } + Err(e) => { + eprintln!( + "[SSH Agent Native Module] Error while starting agent server: {}", + e + ); + } + } + }); + + Ok(agent) + } +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs b/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs new file mode 100644 index 00000000000..fd6d9dacb9f --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs @@ -0,0 +1,41 @@ +use bitwarden_russh::ssh_agent; +pub mod named_pipe_listener_stream; + +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; +use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; + +use super::BitwardenDesktopAgent; + +impl BitwardenDesktopAgent { + pub async fn start_server( + auth_request_tx: tokio::sync::mpsc::Sender<(u32, String)>, + auth_response_rx: Arc>>, + ) -> Result { + let agent_state = BitwardenDesktopAgent { + keystore: ssh_agent::KeyStore(Arc::new(RwLock::new(HashMap::new()))), + show_ui_request_tx: auth_request_tx, + get_ui_response_rx: auth_response_rx, + cancellation_token: CancellationToken::new(), + request_id: Arc::new(tokio::sync::Mutex::new(0)), + }; + let stream = named_pipe_listener_stream::NamedPipeServerStream::new( + agent_state.cancellation_token.clone(), + ); + + let cloned_agent_state = agent_state.clone(); + tokio::spawn(async move { + let _ = ssh_agent::serve( + stream, + cloned_agent_state.clone(), + cloned_agent_state.keystore.clone(), + cloned_agent_state.cancellation_token.clone(), + ) + .await; + }); + Ok(agent_state) + } +} diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 787f22ef37d..bf7701a6566 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -14,12 +14,18 @@ default = [] manual_test = [] [dependencies] -anyhow = "=1.0.86" +base64 = "=0.22.1" +hex = "=0.4.3" +anyhow = "=1.0.93" desktop_core = { path = "../core" } -napi = { version = "=2.16.11", features = ["async"] } +napi = { version = "=2.16.13", features = ["async"] } napi-derive = "=2.16.12" -tokio = { version = "1.38.0" } -tokio-util = "0.7.11" +tokio = { version = "=1.40.0" } +tokio-util = "=0.7.12" +tokio-stream = "=0.1.15" + +[target.'cfg(windows)'.dependencies] +windows-registry = "=0.3.0" [build-dependencies] napi-build = "=2.1.3" diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 45191a48eb0..6d1a7b8abbc 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -42,6 +42,41 @@ export declare namespace clipboards { export function read(): Promise export function write(text: string, password: boolean): Promise } +export declare namespace sshagent { + export interface PrivateKey { + privateKey: string + name: string + cipherId: string + } + export interface SshKey { + privateKey: string + publicKey: string + keyFingerprint: string + } + export const enum SshKeyImportStatus { + /** ssh key was parsed correctly and will be returned in the result */ + Success = 0, + /** ssh key was parsed correctly but is encrypted and requires a password */ + PasswordRequired = 1, + /** ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect */ + WrongPassword = 2, + /** ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key */ + ParsingError = 3, + /** ssh key type is not supported (e.g. ecdsa) */ + UnsupportedKeyType = 4 + } + export interface SshKeyImportResult { + status: SshKeyImportStatus + sshKey?: SshKey + } + export function serve(callback: (err: Error | null, arg: string) => any): Promise + export function stop(agentState: SshAgentState): void + export function setKeys(agentState: SshAgentState, newKeys: Array): void + export function lock(agentState: SshAgentState): void + export function importKey(encodedKey: string, password: string): SshKeyImportResult + export function generateKeypair(keyAlgorithm: string): Promise + export class SshAgentState { } +} export declare namespace processisolations { export function disableCoredumps(): Promise export function isCoreDumpingDisabled(): Promise @@ -51,6 +86,10 @@ export declare namespace powermonitors { export function onLock(callback: (err: Error | null, ) => any): Promise export function isLockMonitorAvailable(): Promise } +export declare namespace windows_registry { + export function createKey(key: string, subkey: string, value: string): Promise + export function deleteKey(key: string, subkey: string): Promise +} export declare namespace ipc { export interface IpcMessage { clientId: number diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 838eb651244..60a8326a8e5 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -1,5 +1,8 @@ #[macro_use] extern crate napi_derive; + +mod registry; + #[napi] pub mod passwords { /// Fetch the stored password from the keychain. @@ -51,12 +54,16 @@ pub mod biometrics { hwnd: napi::bindgen_prelude::Buffer, message: String, ) -> napi::Result { - Biometric::prompt(hwnd.into(), message).await.map_err(|e| napi::Error::from_reason(e.to_string())) + Biometric::prompt(hwnd.into(), message) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) } #[napi] pub async fn available() -> napi::Result { - Biometric::available().await.map_err(|e| napi::Error::from_reason(e.to_string())) + Biometric::available() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) } #[napi] @@ -148,6 +155,199 @@ pub mod clipboards { } } +#[napi] +pub mod sshagent { + use std::sync::Arc; + + use napi::{ + bindgen_prelude::Promise, + threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction}, + }; + use tokio::{self, sync::Mutex}; + + #[napi] + pub struct SshAgentState { + state: desktop_core::ssh_agent::BitwardenDesktopAgent, + } + + #[napi(object)] + pub struct PrivateKey { + pub private_key: String, + pub name: String, + pub cipher_id: String, + } + + #[napi(object)] + pub struct SshKey { + pub private_key: String, + pub public_key: String, + pub key_fingerprint: String, + } + + impl From for SshKey { + fn from(key: desktop_core::ssh_agent::importer::SshKey) -> Self { + SshKey { + private_key: key.private_key, + public_key: key.public_key, + key_fingerprint: key.key_fingerprint, + } + } + } + + #[napi] + pub enum SshKeyImportStatus { + /// ssh key was parsed correctly and will be returned in the result + Success, + /// ssh key was parsed correctly but is encrypted and requires a password + PasswordRequired, + /// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect + WrongPassword, + /// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key + ParsingError, + /// ssh key type is not supported (e.g. ecdsa) + UnsupportedKeyType, + } + + impl From for SshKeyImportStatus { + fn from(status: desktop_core::ssh_agent::importer::SshKeyImportStatus) -> Self { + match status { + desktop_core::ssh_agent::importer::SshKeyImportStatus::Success => { + SshKeyImportStatus::Success + } + desktop_core::ssh_agent::importer::SshKeyImportStatus::PasswordRequired => { + SshKeyImportStatus::PasswordRequired + } + desktop_core::ssh_agent::importer::SshKeyImportStatus::WrongPassword => { + SshKeyImportStatus::WrongPassword + } + desktop_core::ssh_agent::importer::SshKeyImportStatus::ParsingError => { + SshKeyImportStatus::ParsingError + } + desktop_core::ssh_agent::importer::SshKeyImportStatus::UnsupportedKeyType => { + SshKeyImportStatus::UnsupportedKeyType + } + } + } + } + + #[napi(object)] + pub struct SshKeyImportResult { + pub status: SshKeyImportStatus, + pub ssh_key: Option, + } + + impl From for SshKeyImportResult { + fn from(result: desktop_core::ssh_agent::importer::SshKeyImportResult) -> Self { + SshKeyImportResult { + status: result.status.into(), + ssh_key: result.ssh_key.map(|k| k.into()), + } + } + } + + #[napi] + pub async fn serve( + callback: ThreadsafeFunction, + ) -> napi::Result { + let (auth_request_tx, mut auth_request_rx) = tokio::sync::mpsc::channel::<(u32, String)>(32); + let (auth_response_tx, auth_response_rx) = tokio::sync::broadcast::channel::<(u32, bool)>(32); + let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); + tokio::spawn(async move { + let _ = auth_response_rx; + + while let Some((request_id, cipher_uuid)) = auth_request_rx.recv().await { + let cloned_request_id = request_id.clone(); + let cloned_cipher_uuid = cipher_uuid.clone(); + let cloned_response_tx_arc = auth_response_tx_arc.clone(); + let cloned_callback = callback.clone(); + tokio::spawn(async move { + let request_id = cloned_request_id; + let cipher_uuid = cloned_cipher_uuid; + let auth_response_tx_arc = cloned_response_tx_arc; + let callback = cloned_callback; + let promise_result: Result, napi::Error> = + callback.call_async(Ok(cipher_uuid)).await; + match promise_result { + Ok(promise_result) => match promise_result.await { + Ok(result) => { + let _ = auth_response_tx_arc.lock().await.send((request_id, result)) + .expect("should be able to send auth response to agent"); + } + Err(e) => { + println!("[SSH Agent Native Module] calling UI callback promise was rejected: {}", e); + let _ = auth_response_tx_arc.lock().await.send((request_id, false)) + .expect("should be able to send auth response to agent"); + } + }, + Err(e) => { + println!("[SSH Agent Native Module] calling UI callback could not create promise: {}", e); + let _ = auth_response_tx_arc.lock().await.send((request_id, false)) + .expect("should be able to send auth response to agent"); + } + } + }); + } + }); + + match desktop_core::ssh_agent::BitwardenDesktopAgent::start_server( + auth_request_tx, + Arc::new(Mutex::new(auth_response_rx)), + ) + .await + { + Ok(state) => Ok(SshAgentState { state }), + Err(e) => Err(napi::Error::from_reason(e.to_string())), + } + } + + #[napi] + pub fn stop(agent_state: &mut SshAgentState) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state.stop(); + Ok(()) + } + + #[napi] + pub fn set_keys( + agent_state: &mut SshAgentState, + new_keys: Vec, + ) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state + .set_keys( + new_keys + .iter() + .map(|k| (k.private_key.clone(), k.name.clone(), k.cipher_id.clone())) + .collect(), + ) + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + Ok(()) + } + + #[napi] + pub fn lock(agent_state: &mut SshAgentState) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state + .lock() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub fn import_key(encoded_key: String, password: String) -> napi::Result { + let result = desktop_core::ssh_agent::importer::import_key(encoded_key, password) + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + Ok(result.into()) + } + + #[napi] + pub async fn generate_keypair(key_algorithm: String) -> napi::Result { + desktop_core::ssh_agent::generator::generate_keypair(key_algorithm) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + .map(|k| k.into()) + } +} + #[napi] pub mod processisolations { #[napi] @@ -169,12 +369,19 @@ pub mod processisolations { #[napi] pub mod powermonitors { - use napi::{threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode}, tokio}; + use napi::{ + threadsafe_function::{ + ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }, + tokio, + }; #[napi] pub async fn on_lock(callback: ThreadsafeFunction<(), CalleeHandled>) -> napi::Result<()> { let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32); - desktop_core::powermonitor::on_lock(tx).await.map_err(|e| napi::Error::from_reason(e.to_string()))?; + desktop_core::powermonitor::on_lock(tx) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; tokio::spawn(async move { while let Some(message) = rx.recv().await { callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking); @@ -187,7 +394,21 @@ pub mod powermonitors { pub async fn is_lock_monitor_available() -> napi::Result { Ok(desktop_core::powermonitor::is_lock_monitor_available().await) } +} +#[napi] +pub mod windows_registry { + #[napi] + pub async fn create_key(key: String, subkey: String, value: String) -> napi::Result<()> { + crate::registry::create_key(&key, &subkey, &value) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn delete_key(key: String, subkey: String) -> napi::Result<()> { + crate::registry::delete_key(&key, &subkey) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } } #[napi] diff --git a/apps/desktop/desktop_native/napi/src/registry/dummy.rs b/apps/desktop/desktop_native/napi/src/registry/dummy.rs new file mode 100644 index 00000000000..8cef50f3aaf --- /dev/null +++ b/apps/desktop/desktop_native/napi/src/registry/dummy.rs @@ -0,0 +1,9 @@ +use anyhow::{bail, Result}; + +pub fn create_key(_key: &str, _subkey: &str, _value: &str) -> Result<()> { + bail!("Not implemented") +} + +pub fn delete_key(_key: &str, _subkey: &str) -> Result<()> { + bail!("Not implemented") +} diff --git a/apps/desktop/desktop_native/napi/src/registry/mod.rs b/apps/desktop/desktop_native/napi/src/registry/mod.rs new file mode 100644 index 00000000000..68929408ec7 --- /dev/null +++ b/apps/desktop/desktop_native/napi/src/registry/mod.rs @@ -0,0 +1,4 @@ +#[cfg_attr(target_os = "windows", path = "windows.rs")] +#[cfg_attr(not(target_os = "windows"), path = "dummy.rs")] +mod internal; +pub use internal::*; diff --git a/apps/desktop/desktop_native/napi/src/registry/windows.rs b/apps/desktop/desktop_native/napi/src/registry/windows.rs new file mode 100644 index 00000000000..481dfb5dc49 --- /dev/null +++ b/apps/desktop/desktop_native/napi/src/registry/windows.rs @@ -0,0 +1,29 @@ +use anyhow::{bail, Result}; + +fn convert_key(key: &str) -> Result<&'static windows_registry::Key> { + Ok(match key.to_uppercase().as_str() { + "HKEY_CURRENT_USER" | "HKCU" => windows_registry::CURRENT_USER, + "HKEY_LOCAL_MACHINE" | "HKLM" => windows_registry::LOCAL_MACHINE, + "HKEY_CLASSES_ROOT" | "HKCR" => windows_registry::CLASSES_ROOT, + _ => bail!("Invalid key"), + }) +} + +pub fn create_key(key: &str, subkey: &str, value: &str) -> Result<()> { + let key = convert_key(key)?; + + let subkey = key.create(subkey)?; + + const DEFAULT: &str = ""; + subkey.set_string(DEFAULT, value)?; + + Ok(()) +} + +pub fn delete_key(key: &str, subkey: &str) -> Result<()> { + let key = convert_key(key)?; + + key.remove_tree(subkey)?; + + Ok(()) +} diff --git a/apps/desktop/desktop_native/proxy/Cargo.toml b/apps/desktop/desktop_native/proxy/Cargo.toml index 6f8005811d6..a1cefca7a3f 100644 --- a/apps/desktop/desktop_native/proxy/Cargo.toml +++ b/apps/desktop/desktop_native/proxy/Cargo.toml @@ -7,7 +7,7 @@ version = "0.0.0" publish = false [dependencies] -anyhow = "=1.0.86" +anyhow = "=1.0.93" desktop_core = { path = "../core", default-features = false } futures = "0.3.30" log = "0.4.22" diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 0e621e8a1f9..53c20b7faf0 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -77,22 +77,19 @@ { "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}", "to": "MacOS/desktop_proxy" + }, + { + "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}", + "to": "MacOS/desktop_proxy.inherit" } ], - "signIgnore": ["MacOS/desktop_proxy"], + "signIgnore": ["MacOS/desktop_proxy", "MacOS/desktop_proxy.inherit"], "target": ["dmg", "zip"] }, "win": { "electronUpdaterCompatibility": ">=0.0.1", "target": ["portable", "nsis-web", "appx"], "sign": "./sign.js", - "extraResources": [ - { - "from": "../../node_modules/regedit/vbs", - "to": "regedit/vbs", - "filter": ["**/*"] - } - ], "extraFiles": [ { "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", diff --git a/apps/desktop/macos/README.md b/apps/desktop/macos/README.md new file mode 100644 index 00000000000..6e016144b4b --- /dev/null +++ b/apps/desktop/macos/README.md @@ -0,0 +1,23 @@ +# MacOS Extensions for Desktop Apps + +This folder contains an Xcode project that builds macOS extensions for our desktop app. The extensions are used to provide additional functionality to the desktop app, such as autofill (password and passkeys). + +## Manage loaded extensions + +macOS automatically loads extensions from apps, even if they have never been used (especially if built with Xcode). This can be confusing when you have multiple copies of the same application. To see where an extension is loaded from, use the following command: + +```bash +# To list all extensions +pluginkit -m -v + +# To list a specific extension +pluginkit -m -v -i com.bitwarden.desktop.autofill-extension +``` + +To unregister an extension, you can either remove it from your filesystem, or use the following command: + +```bash +pluginkit -r +``` + +where the path to the .appex file can be found in the output of the first command. diff --git a/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib new file mode 100644 index 00000000000..ace3497a58b --- /dev/null +++ b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift new file mode 100644 index 00000000000..d5c5cabeee4 --- /dev/null +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -0,0 +1,93 @@ +// +// CredentialProviderViewController.swift +// autofill-extension +// +// Created by Andreas Coroiu on 2023-12-21. +// + +import AuthenticationServices +import os + +class CredentialProviderViewController: ASCredentialProviderViewController { + let logger = Logger() + + /* + Implement this method if your extension supports showing credentials in the QuickType bar. + When the user selects a credential from your app, this method will be called with the + ASPasswordCredentialIdentity your app has previously saved to the ASCredentialIdentityStore. + Provide the password by completing the extension request with the associated ASPasswordCredential. + If using the credential would require showing custom UI for authenticating the user, cancel + the request with error code ASExtensionError.userInteractionRequired. + + override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { + let databaseIsUnlocked = true + if (databaseIsUnlocked) { + let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") + self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) + } else { + self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue)) + } + } + */ + + /* + Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with + ASExtensionError.userInteractionRequired. In this case, the system may present your extension's + UI and call this method. Show appropriate UI for authenticating the user then provide the password + by completing the extension request with the associated ASPasswordCredential. + + override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) { + } + */ + + @IBAction func cancel(_ sender: AnyObject?) { + self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) + } + + @IBAction func passwordSelected(_ sender: AnyObject?) { + let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") + self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) + } + + override func prepareInterfaceForExtensionConfiguration() { + logger.log("[autofill-extension] prepareInterfaceForExtensionConfiguration called") + } + + override func prepareInterface(forPasskeyRegistration registrationRequest: ASCredentialRequest) { + logger.log("[autofill-extension] prepare interface for registration request \(registrationRequest.description)") + +// self.extensionContext.cancelRequest(withError: ExampleError.nope) + } + + override func prepareInterfaceToProvideCredential(for credentialRequest: ASCredentialRequest) { + logger.log("[autofill-extension] prepare interface for credential request \(credentialRequest.description)") + } + + /* + Prepare your UI to list available credentials for the user to choose from. The items in + 'serviceIdentifiers' describe the service the user is logging in to, so your extension can + prioritize the most relevant credentials in the list. + */ + override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) { + logger.log("[autofill-extension] prepareCredentialList for serviceIdentifiers: \(serviceIdentifiers.count)") + + for serviceIdentifier in serviceIdentifiers { + logger.log(" service: \(serviceIdentifier.identifier)") + } + } + + override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) { + logger.log("[autofill-extension] prepareInterfaceToProvideCredential for credentialIdentity: \(credentialIdentity.user)") + } + + override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier], requestParameters: ASPasskeyCredentialRequestParameters) { + logger.log("[autofill-extension] prepareCredentialList(passkey) for serviceIdentifiers: \(serviceIdentifiers.count)") + + for serviceIdentifier in serviceIdentifiers { + logger.log(" service: \(serviceIdentifier.identifier)") + } + + logger.log("request parameters: \(requestParameters.relyingPartyIdentifier)") + } + +} diff --git a/apps/desktop/macos/autofill-extension/Info.plist b/apps/desktop/macos/autofill-extension/Info.plist new file mode 100644 index 00000000000..539cfa35b9d --- /dev/null +++ b/apps/desktop/macos/autofill-extension/Info.plist @@ -0,0 +1,23 @@ + + + + + NSExtension + + NSExtensionAttributes + + ASCredentialProviderExtensionCapabilities + + ProvidesPasskeys + + + ASCredentialProviderExtensionShowsConfigurationUI + + + NSExtensionPointIdentifier + com.apple.authentication-services-credential-provider-ui + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).CredentialProviderViewController + + + diff --git a/apps/desktop/macos/autofill-extension/autofill_extension.entitlements b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements new file mode 100644 index 00000000000..2e600a8d529 --- /dev/null +++ b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.authentication-services.autofill-credential-provider + + com.apple.security.app-sandbox + + + diff --git a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..313b158895c --- /dev/null +++ b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj @@ -0,0 +1,367 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */; }; + E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */; }; + E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 968ED08A2C52A47200FFFEE6 /* Production.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = ""; }; + E1DF713C2B342F6900F29026 /* autofill-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "autofill-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; + E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialProviderViewController.swift; sourceTree = ""; }; + E1DF71442B342F6900F29026 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/CredentialProviderViewController.xib; sourceTree = ""; }; + E1DF71462B342F6900F29026 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E1DF71472B342F6900F29026 /* autofill_extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = autofill_extension.entitlements; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + E1DF71392B342F6900F29026 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + E1DF711D2B342E2800F29026 = { + isa = PBXGroup; + children = ( + 968ED08A2C52A47200FFFEE6 /* Production.xcconfig */, + E1DF71402B342F6900F29026 /* autofill-extension */, + E1DF713D2B342F6900F29026 /* Frameworks */, + E1DF71272B342E2800F29026 /* Products */, + ); + sourceTree = ""; + }; + E1DF71272B342E2800F29026 /* Products */ = { + isa = PBXGroup; + children = ( + E1DF713C2B342F6900F29026 /* autofill-extension.appex */, + ); + name = Products; + sourceTree = ""; + }; + E1DF713D2B342F6900F29026 /* Frameworks */ = { + isa = PBXGroup; + children = ( + E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + E1DF71402B342F6900F29026 /* autofill-extension */ = { + isa = PBXGroup; + children = ( + E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */, + E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */, + E1DF71462B342F6900F29026 /* Info.plist */, + E1DF71472B342F6900F29026 /* autofill_extension.entitlements */, + ); + path = "autofill-extension"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + E1DF713B2B342F6900F29026 /* autofill-extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = E1DF714E2B342F6900F29026 /* Build configuration list for PBXNativeTarget "autofill-extension" */; + buildPhases = ( + E1DF71382B342F6900F29026 /* Sources */, + E1DF71392B342F6900F29026 /* Frameworks */, + E1DF713A2B342F6900F29026 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "autofill-extension"; + productName = "autofill-extension"; + productReference = E1DF713C2B342F6900F29026 /* autofill-extension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E1DF711E2B342E2800F29026 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1510; + LastUpgradeCheck = 1510; + TargetAttributes = { + E1DF713B2B342F6900F29026 = { + CreatedOnToolsVersion = 15.1; + }; + }; + }; + buildConfigurationList = E1DF71212B342E2800F29026 /* Build configuration list for PBXProject "desktop" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = E1DF711D2B342E2800F29026; + productRefGroup = E1DF71272B342E2800F29026 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E1DF713B2B342F6900F29026 /* autofill-extension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + E1DF713A2B342F6900F29026 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + E1DF71382B342F6900F29026 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */ = { + isa = PBXVariantGroup; + children = ( + E1DF71442B342F6900F29026 /* Base */, + ); + name = CredentialProviderViewController.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + E1DF71332B342E2900F29026 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + E1DF71342B342E2900F29026 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + E1DF714C2B342F6900F29026 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = LTZ2PFU5D6; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "autofill-extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = Bitwarden; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.bitwarden.desktop.autofill-extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Bitwarden Desktop Autofill Development 2024"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + E1DF714D2B342F6900F29026 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = LTZ2PFU5D6; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "autofill-extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = Bitwarden; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.bitwarden.desktop.autofill-extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Bitwarden Desktop Autofill Development 2024"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + E1DF71212B342E2800F29026 /* Build configuration list for PBXProject "desktop" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E1DF71332B342E2900F29026 /* Debug */, + E1DF71342B342E2900F29026 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E1DF714E2B342F6900F29026 /* Build configuration list for PBXNativeTarget "autofill-extension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E1DF714C2B342F6900F29026 /* Debug */, + E1DF714D2B342F6900F29026 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = E1DF711E2B342E2800F29026 /* Project object */; +} diff --git a/apps/desktop/macos/desktop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/desktop/macos/desktop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000000..18d981003d6 --- /dev/null +++ b/apps/desktop/macos/desktop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/desktop/macos/production.xcconfig b/apps/desktop/macos/production.xcconfig new file mode 100644 index 00000000000..f06f2bf736e --- /dev/null +++ b/apps/desktop/macos/production.xcconfig @@ -0,0 +1,11 @@ +// +// Production.xcconfig +// desktop +// +// Created by Vince Grassia on 7/25/24. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 +CODE_SIGN_IDENTITY[sdk=macosx*] = 3rd Party Mac Developer Application +PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = Bitwarden Desktop Autofill App Store 2024 diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index b243b51acc9..f51c0a32d9d 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -14,11 +14,11 @@ "module-alias": "2.2.3", "node-ipc": "9.2.1", "ts-node": "10.9.2", - "uuid": "10.0.0", + "uuid": "11.0.3", "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "20.16.11", + "@types/node": "22.9.0", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" } @@ -106,12 +106,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.16.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", - "integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==", + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.19.8" } }, "node_modules/@types/node-ipc": { @@ -125,9 +125,9 @@ } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -421,16 +421,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index ccd480e31b6..23b1dbdafba 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -19,11 +19,11 @@ "module-alias": "2.2.3", "node-ipc": "9.2.1", "ts-node": "10.9.2", - "uuid": "10.0.0", + "uuid": "11.0.3", "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "20.16.11", + "@types/node": "22.9.0", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" }, diff --git a/apps/desktop/native-messaging-test-runner/src/native-message.service.ts b/apps/desktop/native-messaging-test-runner/src/native-message.service.ts index 8cc94c96191..cd84504c630 100644 --- a/apps/desktop/native-messaging-test-runner/src/native-message.service.ts +++ b/apps/desktop/native-messaging-test-runner/src/native-message.service.ts @@ -219,7 +219,11 @@ export default class NativeMessageService { key: string, ): Promise { const sharedKey = await this.getSharedKeyForKey(key); - const decrypted = await this.encryptService.decryptToUtf8(payload, sharedKey); + const decrypted = await this.encryptService.decryptToUtf8( + payload, + sharedKey, + "native-messaging-session", + ); return JSON.parse(decrypted); } diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1c1d7d88312..423650cdcec 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.10.2", + "version": "2024.11.0", "keywords": [ "bitwarden", "password", @@ -23,6 +23,7 @@ "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:watch": "cross-env NODE_ENV=production webpack --config webpack.preload.js --watch", + "build:macos-extension": "node scripts/build-macos-extension.js", "build:main": "cross-env NODE_ENV=production webpack --config webpack.main.js", "build:main:dev": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js", "build:main:watch": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js --watch", @@ -38,6 +39,7 @@ "pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never", "pack:mac:mas": "npm run clean:dist && electron-builder --mac mas --universal -p never", "pack:mac:masdev": "npm run clean:dist && electron-builder --mac mas-dev --universal -p never", + "pack:mac:masdev:with-extension": "npm run clean:dist && npm run build:macos-extension && electron-builder --mac mas-dev --universal -p never", "pack:win": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never -c.win.certificateSubjectName=\"8bit Solutions LLC\"", "pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never", "dist:dir": "npm run build && npm run pack:dir", diff --git a/apps/desktop/resources/com.bitwarden.desktop.desktop b/apps/desktop/resources/com.bitwarden.desktop.desktop new file mode 100644 index 00000000000..d61387f3e82 --- /dev/null +++ b/apps/desktop/resources/com.bitwarden.desktop.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=Bitwarden +Exec=bitwarden %u +Terminal=false +Type=Application +Icon=com.bitwarden.desktop +StartupWMClass=Bitwarden +GenericName=Password Manager +Comment=A secure and free password manager for all of your devices. +MimeType=x-scheme-handler/bitwarden; +Categories=System;Security; diff --git a/apps/desktop/resources/entitlements.desktop_proxy.inherit.plist b/apps/desktop/resources/entitlements.desktop_proxy.inherit.plist new file mode 100644 index 00000000000..794eada1cad --- /dev/null +++ b/apps/desktop/resources/entitlements.desktop_proxy.inherit.plist @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.inherit + + + diff --git a/apps/desktop/resources/entitlements.mac.plist b/apps/desktop/resources/entitlements.mac.plist index 48f7bf5cece..915232b83f6 100644 --- a/apps/desktop/resources/entitlements.mac.plist +++ b/apps/desktop/resources/entitlements.mac.plist @@ -8,5 +8,7 @@ com.apple.security.cs.disable-library-validation + com.apple.developer.authentication-services.autofill-credential-provider + diff --git a/apps/desktop/resources/entitlements.mas.inherit.plist b/apps/desktop/resources/entitlements.mas.inherit.plist index 3ee76423e4c..3634c84f81a 100644 --- a/apps/desktop/resources/entitlements.mas.inherit.plist +++ b/apps/desktop/resources/entitlements.mas.inherit.plist @@ -10,5 +10,7 @@ com.apple.security.cs.disable-library-validation + com.apple.developer.authentication-services.autofill-credential-provider + diff --git a/apps/desktop/resources/entitlements.mas.plist b/apps/desktop/resources/entitlements.mas.plist index d42ade962c3..9ab2d3824a8 100644 --- a/apps/desktop/resources/entitlements.mas.plist +++ b/apps/desktop/resources/entitlements.mas.plist @@ -16,6 +16,8 @@ com.apple.security.files.user-selected.read-write + com.apple.developer.authentication-services.autofill-credential-provider + com.apple.security.temporary-exception.files.home-relative-path.read-write /Library/Application Support/Mozilla/NativeMessagingHosts/ diff --git a/apps/desktop/resources/memory-dump-wrapper.sh b/apps/desktop/resources/memory-dump-wrapper.sh index b62c050683a..6737cc312f4 100644 --- a/apps/desktop/resources/memory-dump-wrapper.sh +++ b/apps/desktop/resources/memory-dump-wrapper.sh @@ -7,6 +7,12 @@ ulimit -c 0 RAW_PATH=$(readlink -f "$0") APP_PATH=$(dirname $RAW_PATH) +# force use of base image libdus in snap +if [ -e "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3" ] +then + export LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libdbus-1.so.3" +fi + # pass through all args $APP_PATH/bitwarden-app "$@" diff --git a/apps/desktop/scripts/after-pack.js b/apps/desktop/scripts/after-pack.js index 08cff76e858..fd16cd5ffbe 100644 --- a/apps/desktop/scripts/after-pack.js +++ b/apps/desktop/scripts/after-pack.js @@ -58,18 +58,46 @@ async function run(context) { id = identities[0].id; } - console.log(`Signing proxy binary before the main bundle, using identity '${id}'`); + console.log( + `Signing proxy binary before the main bundle, using identity '${id}', for build ${context.electronPlatformName}`, + ); const appName = context.packager.appInfo.productFilename; const appPath = `${context.appOutDir}/${appName}.app`; const proxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy"); + const inheritProxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy.inherit"); 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}`, - ); + + if (is_mas) { + 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}`, + ); + + const inheritEntitlementsName = "entitlements.desktop_proxy.inherit.plist"; + const inheritEntitlementsPath = path.join( + __dirname, + "..", + "resources", + inheritEntitlementsName, + ); + child_process.execSync( + `codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${inheritEntitlementsPath} ${inheritProxyPath}`, + ); + } else { + // For non-Appstore builds, we don't need the inherit binary as they are not sandboxed, + // but we sign and include it anyway for consistency. It should be removed once DDG supports the proxy directly. + const entitlementsName = "entitlements.mac.plist"; + const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName); + child_process.execSync( + `codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`, + ); + child_process.execSync( + `codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${inheritProxyPath}`, + ); + } } } diff --git a/apps/desktop/scripts/after-sign.js b/apps/desktop/scripts/after-sign.js index 69c078a13b5..dc60e9d1838 100644 --- a/apps/desktop/scripts/after-sign.js +++ b/apps/desktop/scripts/after-sign.js @@ -15,36 +15,62 @@ async function run(context) { const appName = context.packager.appInfo.productFilename; const appPath = `${context.appOutDir}/${appName}.app`; const macBuild = context.electronPlatformName === "darwin"; - const copyPlugIn = ["darwin", "mas"].includes(context.electronPlatformName); + const copySafariExtension = ["darwin", "mas"].includes(context.electronPlatformName); + const copyAutofillExtension = ["mas"].includes(context.electronPlatformName); - if (copyPlugIn) { + let shouldResign = false; + + // cannot use extraFiles because it modifies the extensions .plist and makes it invalid + if (copyAutofillExtension) { + console.log("### Copying autofill extension"); + const extensionPath = path.join(__dirname, "../macos/dist/autofill-extension.appex"); + if (!fse.existsSync(extensionPath)) { + console.log("### Autofill extension not found - skipping"); + } else { + if (!fse.existsSync(path.join(appPath, "Contents/PlugIns"))) { + fse.mkdirSync(path.join(appPath, "Contents/PlugIns")); + } + fse.copySync(extensionPath, path.join(appPath, "Contents/PlugIns/autofill-extension.appex")); + shouldResign = true; + } + } + + if (copySafariExtension) { + console.log("### Copying safari extension"); // Copy Safari plugin to work-around https://github.com/electron-userland/electron-builder/issues/5552 const plugIn = path.join(__dirname, "../PlugIns"); - if (fse.existsSync(plugIn)) { - fse.mkdirSync(path.join(appPath, "Contents/PlugIns")); + if (!fse.existsSync(plugIn)) { + console.log("### Safari extension not found - skipping"); + } else { + if (!fse.existsSync(path.join(appPath, "Contents/PlugIns"))) { + fse.mkdirSync(path.join(appPath, "Contents/PlugIns")); + } fse.copySync( path.join(plugIn, "safari.appex"), path.join(appPath, "Contents/PlugIns/safari.appex"), ); + shouldResign = true; + } + } - // Resign to sign safari extension - if (context.electronPlatformName === "mas") { - const masBuildOptions = deepAssign( - {}, - context.packager.platformSpecificBuildOptions, - context.packager.config.mas, - ); - if (context.targets.some((e) => e.name === "mas-dev")) { - deepAssign(masBuildOptions, { - type: "development", - }); - } - if (context.packager.packagerOptions.prepackaged == null) { - await context.packager.sign(appPath, context.appOutDir, masBuildOptions, context.arch); - } - } else { - await context.packager.signApp(context, true); + if (shouldResign) { + // Resign to sign safari extension + if (context.electronPlatformName === "mas") { + const masBuildOptions = deepAssign( + {}, + context.packager.platformSpecificBuildOptions, + context.packager.config.mas, + ); + if (context.targets.some((e) => e.name === "mas-dev")) { + deepAssign(masBuildOptions, { + type: "development", + }); } + if (context.packager.packagerOptions.prepackaged == null) { + await context.packager.sign(appPath, context.appOutDir, masBuildOptions, context.arch); + } + } else { + await context.packager.signApp(context, true); } } diff --git a/apps/desktop/scripts/build-macos-extension.js b/apps/desktop/scripts/build-macos-extension.js new file mode 100644 index 00000000000..3aa43fb6785 --- /dev/null +++ b/apps/desktop/scripts/build-macos-extension.js @@ -0,0 +1,62 @@ +/* eslint-disable @typescript-eslint/no-var-requires, no-console */ +const child = require("child_process"); +const { exit } = require("process"); + +const fse = require("fs-extra"); + +const paths = { + macosBuild: "./macos/build", + extensionBuild: "./macos/build/Release/autofill-extension.appex", + extensionDistDir: "./macos/dist", + extensionDist: "./macos/dist/autofill-extension.appex", + macOsProject: "./macos/desktop.xcodeproj", + macOsConfig: "./macos/production.xcconfig", +}; + +async function buildMacOs() { + if (fse.existsSync(paths.macosBuild)) { + fse.removeSync(paths.macosBuild); + } + + if (fse.existsSync(paths.extensionDistDir)) { + fse.removeSync(paths.extensionDistDir); + } + + const proc = child.spawn("xcodebuild", [ + "-project", + paths.macOsProject, + "-alltargets", + "-configuration", + "Release", + "-xcconfig", + paths.macOsConfig, + ]); + stdOutProc(proc); + await new Promise((resolve, reject) => + proc.on("close", (code) => { + if (code > 0) { + console.error("xcodebuild failed with code", code); + return reject(new Error(`xcodebuild failed with code ${code}`)); + } + console.log("xcodebuild success"); + resolve(); + }), + ); + + fse.mkdirSync(paths.extensionDistDir); + fse.copySync(paths.extensionBuild, paths.extensionDist); + // Delete the build dir, otherwise MacOS will load the extension from there instead of the Bitwarden.app bundle + fse.removeSync(paths.macosBuild); +} + +function stdOutProc(proc) { + proc.stdout.on("data", (data) => console.log(data.toString())); + proc.stderr.on("data", (data) => console.error(data.toString())); +} + +buildMacOs() + .then(() => console.log("macOS build complete")) + .catch((err) => { + console.error("macOS build failed", err); + exit(-1); + }); diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 76cf98b1b24..7336ce09dd8 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -419,6 +419,23 @@ "enableHardwareAccelerationDesc" | i18n }}
+
+
+ +
+ {{ + "enableSshAgentDesc" | i18n + }} +
+ +
+
+
+ +
+
+
+
+ + + +
+
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ +
+
+
+ +
+
diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.ts b/apps/desktop/src/vault/app/vault/add-edit.component.ts index e34d9cbb412..a3a9c929159 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -1,6 +1,7 @@ import { DatePipe } from "@angular/common"; -import { Component, NgZone, OnChanges, OnInit, OnDestroy, ViewChild } from "@angular/core"; +import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { NgForm } from "@angular/forms"; +import { sshagent as sshAgent } from "desktop_native/napi"; import { CollectionService } from "@bitwarden/admin-console/common"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component"; @@ -18,8 +19,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; const BroadcasterSubscriptionId = "AddEditComponent"; @@ -31,6 +33,7 @@ const BroadcasterSubscriptionId = "AddEditComponent"; export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnChanges, OnDestroy { @ViewChild("form") private form: NgForm; + constructor( cipherService: CipherService, folderService: FolderService, @@ -51,6 +54,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On dialogService: DialogService, datePipe: DatePipe, configService: ConfigService, + private toastService: ToastService, cipherAuthorizationService: CipherAuthorizationService, ) { super( @@ -140,4 +144,68 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On "https://bitwarden.com/help/managing-items/#protect-individual-items", ); } + + async generateSshKey() { + const sshKey = await ipc.platform.sshAgent.generateKey("ed25519"); + this.cipher.sshKey.privateKey = sshKey.privateKey; + this.cipher.sshKey.publicKey = sshKey.publicKey; + this.cipher.sshKey.keyFingerprint = sshKey.keyFingerprint; + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyGenerated"), + }); + } + + async importSshKeyFromClipboard() { + const key = await this.platformUtilsService.readFromClipboard(); + const parsedKey = await ipc.platform.sshAgent.importKey(key, ""); + if (parsedKey == null || parsedKey.status === sshAgent.SshKeyImportStatus.ParsingError) { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("invalidSshKey"), + }); + return; + } else if (parsedKey.status === sshAgent.SshKeyImportStatus.UnsupportedKeyType) { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("sshKeyTypeUnsupported"), + }); + } else if ( + parsedKey.status === sshAgent.SshKeyImportStatus.PasswordRequired || + parsedKey.status === sshAgent.SshKeyImportStatus.WrongPassword + ) { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("sshKeyPasswordUnsupported"), + }); + return; + } else { + this.cipher.sshKey.privateKey = parsedKey.sshKey.privateKey; + this.cipher.sshKey.publicKey = parsedKey.sshKey.publicKey; + this.cipher.sshKey.keyFingerprint = parsedKey.sshKey.keyFingerprint; + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyPasted"), + }); + } + } + + async typeChange() { + if (this.cipher.type === CipherType.SshKey) { + await this.generateSshKey(); + } + } + + truncateString(value: string, length: number) { + return value.length > length ? value.substring(0, length) + "..." : value; + } + + togglePrivateKey() { + this.showPrivateKey = !this.showPrivateKey; + } } diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html index 381c06e8b67..c3dcd191dfc 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html @@ -79,4 +79,19 @@ +
  • + + + +
  • diff --git a/apps/desktop/src/vault/app/vault/vault.component.ts b/apps/desktop/src/vault/app/vault/vault.component.ts index b6357b35d7b..54d8eb833f4 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.ts +++ b/apps/desktop/src/vault/app/vault/vault.component.ts @@ -428,7 +428,7 @@ export class VaultComponent implements OnInit, OnDestroy { return; } - this.addType = type; + this.addType = type || this.activeFilter.cipherType; this.action = "add"; this.cipherId = null; this.prefillNewCipherFromFilter(); diff --git a/apps/desktop/src/vault/app/vault/view.component.html b/apps/desktop/src/vault/app/vault/view.component.html index c855224cf9f..e6c20d2e89e 100644 --- a/apps/desktop/src/vault/app/vault/view.component.html +++ b/apps/desktop/src/vault/app/vault/view.component.html @@ -399,6 +399,105 @@
    {{ cipher.identity.country }}
    + +
    +
    +
    + {{ "sshPrivateKey" | i18n }} +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    diff --git a/apps/web/config/usdev.json b/apps/web/config/usdev.json index af96a38c6af..c19deba27b1 100644 --- a/apps/web/config/usdev.json +++ b/apps/web/config/usdev.json @@ -4,6 +4,15 @@ "notifications": "https://notifications.usdev.bitwarden.pw", "scim": "https://scim.usdev.bitwarden.pw" }, + "additionalRegions": [ + { + "key": "USDEV", + "domain": "usdev.bitwarden.pw", + "urls": { + "webVault": "https://vault.usdev.bitwarden.pw" + } + } + ], "flags": { "showPasswordless": true } diff --git a/apps/web/package.json b/apps/web/package.json index 05ce76f524a..5dd0e442f2d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.10.4", + "version": "2024.11.0", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts b/apps/web/src/app/admin-console/organizations/core/services/group/group-api.service.ts similarity index 78% rename from apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts rename to apps/web/src/app/admin-console/organizations/core/services/group/group-api.service.ts index e06a9aa8dc7..3b933ab9854 100644 --- a/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts +++ b/apps/web/src/app/admin-console/organizations/core/services/group/group-api.service.ts @@ -6,8 +6,10 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CoreOrganizationModule } from "../../core-organization.module"; +import { GroupDetailsView } from "../../views/group-details.view"; import { GroupView } from "../../views/group.view"; +import { AddEditGroupDetail } from "./../../views/add-edit-group-detail"; import { GroupRequest } from "./requests/group.request"; import { OrganizationGroupBulkRequest } from "./requests/organization-group-bulk.request"; import { GroupDetailsResponse, GroupResponse } from "./responses/group.response"; @@ -15,13 +17,13 @@ import { GroupDetailsResponse, GroupResponse } from "./responses/group.response" @Injectable({ providedIn: "root", }) -export class GroupService { +export class GroupApiService { constructor( protected apiService: ApiService, protected configService: ConfigService, ) {} - async get(orgId: string, groupId: string): Promise { + async get(orgId: string, groupId: string): Promise { const r = await this.apiService.send( "GET", "/organizations/" + orgId + "/groups/" + groupId + "/details", @@ -30,7 +32,7 @@ export class GroupService { true, ); - return GroupView.fromResponse(new GroupDetailsResponse(r)); + return GroupDetailsView.fromResponse(new GroupDetailsResponse(r)); } async getAll(orgId: string): Promise { @@ -44,12 +46,26 @@ export class GroupService { const listResponse = new ListResponse(r, GroupDetailsResponse); - return Promise.all(listResponse.data?.map((gr) => GroupView.fromResponse(gr))) ?? []; + return listResponse.data.map((gr) => GroupView.fromResponse(gr)); + } + + async getAllDetails(orgId: string): Promise { + const r = await this.apiService.send( + "GET", + "/organizations/" + orgId + "/groups/details", + null, + true, + true, + ); + + const listResponse = new ListResponse(r, GroupDetailsResponse); + + return listResponse.data.map((gr) => GroupDetailsView.fromResponse(gr)); } } @Injectable({ providedIn: CoreOrganizationModule }) -export class InternalGroupService extends GroupService { +export class InternalGroupApiService extends GroupApiService { constructor( protected apiService: ApiService, protected configService: ConfigService, @@ -77,7 +93,7 @@ export class InternalGroupService extends GroupService { ); } - async save(group: GroupView): Promise { + async save(group: AddEditGroupDetail): Promise { const request = new GroupRequest(); request.name = group.name; request.users = group.members; diff --git a/apps/web/src/app/admin-console/organizations/core/services/index.ts b/apps/web/src/app/admin-console/organizations/core/services/index.ts index 627cb2416ae..88cd6f8763c 100644 --- a/apps/web/src/app/admin-console/organizations/core/services/index.ts +++ b/apps/web/src/app/admin-console/organizations/core/services/index.ts @@ -1,2 +1,2 @@ -export * from "./group/group.service"; +export * from "./group/group-api.service"; export * from "./user-admin.service"; diff --git a/apps/web/src/app/admin-console/organizations/core/views/add-edit-group-detail.ts b/apps/web/src/app/admin-console/organizations/core/views/add-edit-group-detail.ts new file mode 100644 index 00000000000..83fe65c07a9 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/core/views/add-edit-group-detail.ts @@ -0,0 +1,10 @@ +import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; + +export interface AddEditGroupDetail { + id: string; + organizationId: string; + name: string; + externalId: string; + collections: CollectionAccessSelectionView[]; + members: string[]; +} diff --git a/apps/web/src/app/admin-console/organizations/core/views/group-details.view.ts b/apps/web/src/app/admin-console/organizations/core/views/group-details.view.ts new file mode 100644 index 00000000000..efa6b9daf79 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/core/views/group-details.view.ts @@ -0,0 +1,20 @@ +import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; +import { View } from "@bitwarden/common/models/view/view"; + +import { GroupDetailsResponse } from "../services/group/responses/group.response"; + +export class GroupDetailsView implements View { + id: string; + organizationId: string; + name: string; + externalId: string; + collections: CollectionAccessSelectionView[] = []; + + static fromResponse(response: GroupDetailsResponse): GroupDetailsView { + const view: GroupDetailsView = Object.assign(new GroupDetailsView(), response); + + view.collections = response.collections.map((c) => new CollectionAccessSelectionView(c)); + + return view; + } +} diff --git a/apps/web/src/app/admin-console/organizations/core/views/group.view.ts b/apps/web/src/app/admin-console/organizations/core/views/group.view.ts index 2566e4c4bfa..10ec61142ce 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/group.view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/group.view.ts @@ -1,23 +1,14 @@ -import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; import { View } from "@bitwarden/common/models/view/view"; -import { GroupDetailsResponse, GroupResponse } from "../services/group/responses/group.response"; +import { GroupResponse } from "../services/group/responses/group.response"; export class GroupView implements View { id: string; organizationId: string; name: string; externalId: string; - collections: CollectionAccessSelectionView[] = []; - members: string[] = []; static fromResponse(response: GroupResponse): GroupView { - const view: GroupView = Object.assign(new GroupView(), response) as GroupView; - - if (response instanceof GroupDetailsResponse && response.collections != undefined) { - view.collections = response.collections.map((c) => new CollectionAccessSelectionView(c)); - } - - return view; + return Object.assign(new GroupView(), response); } } diff --git a/apps/web/src/app/admin-console/organizations/core/views/index.ts b/apps/web/src/app/admin-console/organizations/core/views/index.ts index 9408d7757c3..847b2271766 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/index.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/index.ts @@ -1,3 +1,4 @@ export * from "./group.view"; +export * from "./group-details.view"; export * from "./organization-user.view"; export * from "./organization-user-admin-view"; diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index 23e9c6df178..9cd94c5208c 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -40,9 +40,9 @@ > ; hideNewOrgButton$: Observable; organizationIsUnmanaged$: Observable; - isAccessIntelligenceFeatureEnabled = false; + isRiskInsightsFeatureEnabled = false; private _destroy = new Subject(); - protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableConsolidatedBilling, - ); - constructor( private route: ActivatedRoute, private organizationService: OrganizationService, @@ -71,7 +67,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { async ngOnInit() { document.body.classList.remove("layout_frontend"); - this.isAccessIntelligenceFeatureEnabled = await this.configService.getFeatureFlag( + this.isRiskInsightsFeatureEnabled = await this.configService.getFeatureFlag( FeatureFlag.AccessIntelligence, ); @@ -101,14 +97,9 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { switchMap((organization) => this.providerService.get$(organization.providerId)), ); - this.organizationIsUnmanaged$ = combineLatest([ - this.consolidatedBillingEnabled$, - this.organization$, - provider$, - ]).pipe( + this.organizationIsUnmanaged$ = combineLatest([this.organization$, provider$]).pipe( map( - ([consolidatedBillingEnabled, organization, provider]) => - !consolidatedBillingEnabled || + ([organization, provider]) => !organization.hasProvider || !provider || provider.providerStatus !== ProviderStatusType.Billable, diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index 643e76e4c38..c16b2e57241 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -30,7 +30,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { UserId } from "@bitwarden/common/types/guid"; import { DialogService, ToastService } from "@bitwarden/components"; -import { InternalGroupService as GroupService, GroupView } from "../core"; +import { InternalGroupApiService as GroupService } from "../core"; import { AccessItemType, AccessItemValue, @@ -40,6 +40,8 @@ import { PermissionMode, } from "../shared/components/access-selector"; +import { AddEditGroupDetail } from "./../core/views/add-edit-group-detail"; + /** * Indices for the available tabs in the dialog */ @@ -105,7 +107,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { title: string; collections: AccessItemView[] = []; members: Array = []; - group: GroupView; + group: AddEditGroupDetail; groupForm = this.formBuilder.group({ name: ["", [Validators.required, Validators.maxLength(100)]], @@ -149,7 +151,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { ); } - private groupDetails$: Observable = of(this.editMode).pipe( + private groupDetails$: Observable = of(this.editMode).pipe( concatMap((editMode) => { if (!editMode) { return of(undefined); @@ -159,9 +161,11 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { this.groupService.get(this.organizationId, this.groupId), this.apiService.getGroupUsers(this.organizationId, this.groupId), ]).pipe( - map(([groupView, users]) => { - groupView.members = users; - return groupView; + map(([groupView, users]): AddEditGroupDetail => { + return { + ...groupView, + members: users, + }; }), catchError((e: unknown) => { if (e instanceof ErrorResponse) { @@ -295,14 +299,16 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { return; } - const groupView = new GroupView(); - groupView.id = this.groupId; - groupView.organizationId = this.organizationId; - const formValue = this.groupForm.value; - groupView.name = formValue.name; - groupView.members = formValue.members?.map((m) => m.id) ?? []; - groupView.collections = formValue.collections.map((c) => convertToSelectionView(c)); + + const groupView: AddEditGroupDetail = { + id: this.groupId, + organizationId: this.organizationId, + name: formValue.name, + members: formValue.members?.map((m) => m.id) ?? [], + collections: formValue.collections.map((c) => convertToSelectionView(c)), + externalId: formValue.externalId, + }; await this.groupService.save(groupView); @@ -346,7 +352,10 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { /** * Maps the group's current collection access to AccessItemValues to populate the access-selector's FormControl */ -function mapToAccessSelections(group: GroupView, items: AccessItemView[]): AccessItemValue[] { +function mapToAccessSelections( + group: AddEditGroupDetail, + items: AccessItemView[], +): AccessItemValue[] { return ( group.collections // The FormControl value only represents editable collection access - exclude readonly access selections @@ -365,7 +374,7 @@ function mapToAccessSelections(group: GroupView, items: AccessItemView[]): Acces function mapToAccessItemViews( collections: CollectionAdminView[], organization: Organization, - group?: GroupView, + group?: AddEditGroupDetail, ): AccessItemView[] { return ( collections diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts index 4d8ad6370ba..10670e999d5 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts @@ -28,7 +28,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { DialogService, TableDataSource, ToastService } from "@bitwarden/components"; -import { InternalGroupService as GroupService, GroupView } from "../core"; +import { GroupDetailsView, InternalGroupApiService as GroupService } from "../core"; import { GroupAddEditDialogResultType, @@ -40,7 +40,7 @@ type GroupDetailsRow = { /** * Details used for displaying group information */ - details: GroupView; + details: GroupDetailsView; /** * True if the group is selected in the table @@ -108,7 +108,7 @@ export class GroupsComponent { ), // groups this.refreshGroups$.pipe( - switchMap(() => this.groupService.getAll(this.organizationId)), + switchMap(() => this.groupService.getAllDetails(this.organizationId)), ), ]), ), diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html index 2c5daf93c6f..c77c8fc935f 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html @@ -262,7 +262,21 @@ {{ "revokeAccess" | i18n }} + - + diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index db1d4d42dfc..039fce5789b 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -56,7 +56,7 @@ import { } from "../../../billing/organizations/change-plan-dialog.component"; import { BaseMembersComponent } from "../../common/base-members.component"; import { PeopleTableDataSource } from "../../common/people-table-data-source"; -import { GroupService } from "../core"; +import { GroupApiService } from "../core"; import { OrganizationUserView } from "../core/views/organization-user.view"; import { openEntityEventsDialog } from "../manage/entity-events.component"; @@ -102,6 +102,10 @@ export class MembersComponent extends BaseMembersComponent FeatureFlag.EnableUpgradePasswordManagerSub, ); + protected accountDeprovisioningEnabled$: Observable = this.configService.getFeatureFlag$( + FeatureFlag.AccountDeprovisioning, + ); + // Fixed sizes used for cdkVirtualScroll protected rowHeight = 69; protected rowHeightClass = `tw-h-[69px]`; @@ -125,7 +129,7 @@ export class MembersComponent extends BaseMembersComponent private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserApiService: OrganizationUserApiService, private router: Router, - private groupService: GroupService, + private groupService: GroupApiService, private collectionService: CollectionService, private billingApiService: BillingApiServiceAbstraction, private modalService: ModalService, @@ -486,7 +490,7 @@ export class MembersComponent extends BaseMembersComponent const enableUpgradePasswordManagerSub = await firstValueFrom( this.enableUpgradePasswordManagerSub$, ); - if (enableUpgradePasswordManagerSub) { + if (enableUpgradePasswordManagerSub && this.organization.canEditSubscription) { const reference = openChangePlanDialog(this.dialogService, { data: { organizationId: this.organization.id, @@ -518,6 +522,7 @@ export class MembersComponent extends BaseMembersComponent isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, initialTab: initialTab, numConfirmedMembers: this.dataSource.confirmedUserCount, + managedByOrganization: user?.managedByOrganization, }, }); @@ -725,6 +730,40 @@ export class MembersComponent extends BaseMembersComponent return true; } + async deleteUser(user: OrganizationUserView) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { + key: "deleteOrganizationUser", + placeholders: [this.userNamePipe.transform(user)], + }, + content: { key: "deleteOrganizationUserWarning" }, + type: "warning", + acceptButtonText: { key: "delete" }, + cancelButtonText: { key: "cancel" }, + }); + + if (!confirmed) { + return false; + } + + this.actionPromise = this.organizationUserApiService.deleteOrganizationUser( + this.organization.id, + user.id, + ); + try { + await this.actionPromise; + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("organizationUserDeleted", this.userNamePipe.transform(user)), + }); + this.dataSource.removeUser(user); + } catch (e) { + this.validationService.showError(e); + } + this.actionPromise = null; + } + private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) { return this.dialogService.openSimpleDialog({ title: { diff --git a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts index a36b267e2fe..1725148d477 100644 --- a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts @@ -63,10 +63,10 @@ const routes: Routes = [ ), }, { - path: "access-intelligence", + path: "risk-insights", loadChildren: () => - import("../../tools/access-intelligence/access-intelligence.module").then( - (m) => m.AccessIntelligenceModule, + import("../../tools/risk-insights/risk-insights.module").then( + (m) => m.RiskInsightsModule, ), }, { diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html index 20f62c1be0b..f656d488e06 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html @@ -15,7 +15,7 @@
    - - - {{ "cancel" | i18n }} - - - - - - - diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index f1b079b81aa..704960c14c6 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -104,7 +104,7 @@ export class AppComponent implements OnDestroy, OnInit { .subscribe((supported) => { if (!supported) { this.logService.debug("SDK is not supported"); - this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); + this.sdkService.failedToInitialize("web").catch((e) => this.logService.error(e)); } else { this.logService.debug("SDK is supported"); } diff --git a/apps/web/src/app/auth/settings/account/account.component.html b/apps/web/src/app/auth/settings/account/account.component.html index 71508f7ae97..4055f14219c 100644 --- a/apps/web/src/app/auth/settings/account/account.component.html +++ b/apps/web/src/app/auth/settings/account/account.component.html @@ -3,7 +3,7 @@ -
    +

    {{ "changeEmail" | i18n }}

    @@ -21,7 +21,13 @@ > {{ "purgeVault" | i18n }} - diff --git a/apps/web/src/app/auth/settings/account/account.component.ts b/apps/web/src/app/auth/settings/account/account.component.ts index dd8dc881f6e..eed88476e27 100644 --- a/apps/web/src/app/auth/settings/account/account.component.ts +++ b/apps/web/src/app/auth/settings/account/account.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; -import { lastValueFrom, map, Observable, of, switchMap } from "rxjs"; +import { combineLatest, from, lastValueFrom, map, Observable } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -21,8 +21,9 @@ export class AccountComponent implements OnInit { @ViewChild("deauthorizeSessionsTemplate", { read: ViewContainerRef, static: true }) deauthModalRef: ViewContainerRef; - showChangeEmail = true; + showChangeEmail$: Observable; showPurgeVault$: Observable; + showDeleteAccount$: Observable; constructor( private modalService: ModalService, @@ -33,21 +34,46 @@ export class AccountComponent implements OnInit { ) {} async ngOnInit() { - this.showChangeEmail = await this.userVerificationService.hasMasterPassword(); - this.showPurgeVault$ = this.configService - .getFeatureFlag$(FeatureFlag.AccountDeprovisioning) - .pipe( - switchMap((isAccountDeprovisioningEnabled) => - isAccountDeprovisioningEnabled - ? this.organizationService.organizations$.pipe( - map( - (organizations) => - !organizations.some((o) => o.userIsManagedByOrganization === true), - ), - ) - : of(true), - ), - ); + const isAccountDeprovisioningEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.AccountDeprovisioning, + ); + + const userIsManagedByOrganization$ = this.organizationService.organizations$.pipe( + map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)), + ); + + const hasMasterPassword$ = from(this.userVerificationService.hasMasterPassword()); + + this.showChangeEmail$ = combineLatest([ + hasMasterPassword$, + isAccountDeprovisioningEnabled$, + userIsManagedByOrganization$, + ]).pipe( + map( + ([hasMasterPassword, isAccountDeprovisioningEnabled, userIsManagedByOrganization]) => + hasMasterPassword && (!isAccountDeprovisioningEnabled || !userIsManagedByOrganization), + ), + ); + + this.showPurgeVault$ = combineLatest([ + isAccountDeprovisioningEnabled$, + userIsManagedByOrganization$, + ]).pipe( + map( + ([isAccountDeprovisioningEnabled, userIsManagedByOrganization]) => + !isAccountDeprovisioningEnabled || !userIsManagedByOrganization, + ), + ); + + this.showDeleteAccount$ = combineLatest([ + isAccountDeprovisioningEnabled$, + userIsManagedByOrganization$, + ]).pipe( + map( + ([isAccountDeprovisioningEnabled, userIsManagedByOrganization]) => + !isAccountDeprovisioningEnabled || !userIsManagedByOrganization, + ), + ); } async deauthorizeSessions() { diff --git a/apps/web/src/app/auth/settings/account/danger-zone.component.html b/apps/web/src/app/auth/settings/account/danger-zone.component.html index 14c3b7e0b78..1e7c73a3cc6 100644 --- a/apps/web/src/app/auth/settings/account/danger-zone.component.html +++ b/apps/web/src/app/auth/settings/account/danger-zone.component.html @@ -1,9 +1,15 @@

    {{ "dangerZone" | i18n }}

    -

    {{ "dangerZoneDesc" | i18n }}

    +

    + {{ + (accountDeprovisioningEnabled$ | async) && content.children.length === 1 + ? ("dangerZoneDescSingular" | i18n) + : ("dangerZoneDesc" | i18n) + }} +

    -
    +
    diff --git a/apps/web/src/app/auth/settings/account/danger-zone.component.ts b/apps/web/src/app/auth/settings/account/danger-zone.component.ts index 42f198f4f05..4d1adddd183 100644 --- a/apps/web/src/app/auth/settings/account/danger-zone.component.ts +++ b/apps/web/src/app/auth/settings/account/danger-zone.component.ts @@ -1,6 +1,10 @@ -import { Component } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { Observable } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { TypographyModule } from "@bitwarden/components"; /** @@ -10,6 +14,15 @@ import { TypographyModule } from "@bitwarden/components"; selector: "app-danger-zone", templateUrl: "danger-zone.component.html", standalone: true, - imports: [TypographyModule, JslibModule], + imports: [TypographyModule, JslibModule, CommonModule], }) -export class DangerZoneComponent {} +export class DangerZoneComponent implements OnInit { + constructor(private configService: ConfigService) {} + accountDeprovisioningEnabled$: Observable; + + ngOnInit(): void { + this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.AccountDeprovisioning, + ); + } +} diff --git a/apps/web/src/app/auth/settings/account/danger-zone.stories.ts b/apps/web/src/app/auth/settings/account/danger-zone.stories.ts index 67e7f195f12..bbef5e6627d 100644 --- a/apps/web/src/app/auth/settings/account/danger-zone.stories.ts +++ b/apps/web/src/app/auth/settings/account/danger-zone.stories.ts @@ -1,22 +1,33 @@ +import { CommonModule } from "@angular/common"; import { importProvidersFrom } from "@angular/core"; import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ButtonModule } from "@bitwarden/components"; import { PreloadedEnglishI18nModule } from "../../../core/tests"; import { DangerZoneComponent } from "./danger-zone.component"; +class MockConfigService implements Partial {} + export default { title: "Web/Danger Zone", component: DangerZoneComponent, decorators: [ moduleMetadata({ - imports: [ButtonModule, JslibModule], + imports: [ButtonModule, JslibModule, CommonModule], }), applicationConfig({ - providers: [importProvidersFrom(PreloadedEnglishI18nModule)], + providers: [ + importProvidersFrom(PreloadedEnglishI18nModule), + { + provide: ConfigService, + useClass: MockConfigService, + multi: true, + }, + ], }), ], } as Meta; diff --git a/apps/web/src/app/auth/settings/account/profile.component.html b/apps/web/src/app/auth/settings/account/profile.component.html index 93025420b26..e6b69807339 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.html +++ b/apps/web/src/app/auth/settings/account/profile.component.html @@ -36,6 +36,12 @@ Customize
    +
    + {{ "accountIsManagedMessage" | i18n: managingOrganization?.name }} + + + +
    ; private destroy$ = new Subject(); protected formGroup = new FormGroup({ @@ -32,6 +37,8 @@ export class ProfileComponent implements OnInit, OnDestroy { private accountService: AccountService, private dialogService: DialogService, private toastService: ToastService, + private configService: ConfigService, + private organizationService: OrganizationService, ) {} async ngOnInit() { @@ -40,6 +47,19 @@ export class ProfileComponent implements OnInit, OnDestroy { this.fingerprintMaterial = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + this.managingOrganization$ = this.configService + .getFeatureFlag$(FeatureFlag.AccountDeprovisioning) + .pipe( + switchMap((isAccountDeprovisioningEnabled) => + isAccountDeprovisioningEnabled + ? this.organizationService.organizations$.pipe( + map((organizations) => + organizations.find((o) => o.userIsManagedByOrganization === true), + ), + ) + : of(null), + ), + ); this.formGroup.get("name").setValue(this.profile.name); this.formGroup.get("email").setValue(this.profile.email); diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 3406c2d5b61..f5f3e80b6bb 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -194,7 +194,7 @@ export class ChangePasswordComponent HashPurpose.LocalAuthorization, ); - const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); + const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey, userId); if (userKey == null) { this.toastService.showToast({ variant: "error", diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.html b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.html index 23982e8328f..01c0e94dc0f 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.html +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.html @@ -1,7 +1,7 @@
    {{ "loginWithPasskey" | i18n }} + >{{ "logInWithPasskey" | i18n }} {{ "newPasskey" | i18n }} diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html index e65899e05ea..9965302d15a 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html @@ -1,5 +1,5 @@

    - {{ "loginWithPasskey" | i18n }} + {{ "logInWithPasskey" | i18n }} diff --git a/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts b/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts index 2c8b579b994..bc354009775 100644 --- a/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts +++ b/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts @@ -46,7 +46,7 @@ export class SecretsManagerTrialFreeStepperComponent implements OnInit { protected formBuilder: UntypedFormBuilder, protected i18nService: I18nService, protected organizationBillingService: OrganizationBillingService, - private router: Router, + protected router: Router, ) {} ngOnInit(): void { diff --git a/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html b/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html index 1acf4c32097..aeec49e5276 100644 --- a/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html +++ b/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html @@ -22,12 +22,29 @@ bitButton buttonType="primary" [disabled]="formGroup.get('name').invalid" + [loading]="createOrganizationLoading" + (click)="createOrganizationOnTrial()" + *ngIf="enableTrialPayment$ | async" + > + {{ "startTrial" | i18n }} + + - + (); + protected enableTrialPayment$ = this.configService.getFeatureFlag$( + FeatureFlag.TrialPaymentOptional, + ); + + constructor( + private route: ActivatedRoute, + private configService: ConfigService, + protected formBuilder: UntypedFormBuilder, + protected i18nService: I18nService, + protected organizationBillingService: OrganizationBillingService, + protected router: Router, + ) { + super(formBuilder, i18nService, organizationBillingService, router); + } + + async ngOnInit(): Promise { + this.referenceEventRequest = new ReferenceEventRequest(); + this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website"; + + this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => { + if (trialFlowOrgs.includes(qParams.org)) { + if (qParams.org === ValidOrgParams.teamsStarter) { + this.plan = PlanType.TeamsStarter; + } else if (qParams.org === ValidOrgParams.teams) { + this.plan = PlanType.TeamsAnnually; + } else if (qParams.org === ValidOrgParams.enterprise) { + this.plan = PlanType.EnterpriseAnnually; + } + } + }); + } + organizationCreated(event: OrganizationCreatedEvent) { this.organizationId = event.organizationId; this.billingSubLabel = event.planDescription; @@ -31,6 +85,29 @@ export class SecretsManagerTrialPaidStepperComponent extends SecretsManagerTrial this.verticalStepper.previous(); } + async createOrganizationOnTrial(): Promise { + this.createOrganizationLoading = true; + const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({ + organization: { + name: this.formGroup.get("name").value, + billingEmail: this.formGroup.get("email").value, + initiationPath: "Secrets Manager trial from marketing website", + }, + plan: { + type: this.plan, + subscribeToSecretsManager: true, + isFromSecretsManagerTrial: true, + passwordManagerSeats: 1, + secretsManagerSeats: 1, + }, + }); + + this.organizationId = response?.id; + this.subLabels.organizationInfo = response?.name; + this.createOrganizationLoading = false; + this.verticalStepper.next(); + } + get createAccountLabel() { const organizationType = this.productType === ProductTierType.TeamsStarter diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html index ed1dc6cda9b..077836a7634 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html @@ -91,12 +91,17 @@ bitButton buttonType="primary" [disabled]="orgInfoFormGroup.get('name').invalid" - cdkStepperNext + [loading]="loading" + (click)="createOrganizationOnTrial()" > - {{ "next" | i18n }} + {{ (enableTrialPayment$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }} - + { let policyServiceMock: MockProxy; let routerServiceMock: MockProxy; let acceptOrgInviteServiceMock: MockProxy; + let organizationBillingServiceMock: MockProxy; + let configServiceMock: MockProxy; beforeEach(() => { // only define services directly that we want to mock return values in this component @@ -47,6 +51,8 @@ describe("TrialInitiationComponent", () => { policyServiceMock = mock(); routerServiceMock = mock(); acceptOrgInviteServiceMock = mock(); + organizationBillingServiceMock = mock(); + configServiceMock = mock(); // 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 @@ -92,6 +98,14 @@ describe("TrialInitiationComponent", () => { provide: AcceptOrganizationInviteService, useValue: acceptOrgInviteServiceMock, }, + { + provide: OrganizationBillingService, + useValue: organizationBillingServiceMock, + }, + { + provide: ConfigService, + useValue: configServiceMock, + }, ], schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component) }).compileComponents(); diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts index f8718b0a420..7892283a387 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts @@ -9,8 +9,15 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { + OrganizationInformation, + PlanInformation, + OrganizationBillingServiceAbstraction as OrganizationBillingService, +} from "@bitwarden/common/billing/abstractions/organization-billing.service"; import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -25,7 +32,7 @@ import { OrganizationInvite } from "../organization-invite/organization-invite"; import { RouterService } from "./../../core/router.service"; import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component"; -enum ValidOrgParams { +export enum ValidOrgParams { families = "families", enterprise = "enterprise", teams = "teams", @@ -69,6 +76,7 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { productTier: ProductTierType; accountCreateOnly = true; useTrialStepper = false; + loading = false; policies: Policy[]; enforcedPolicyOptions: MasterPasswordPolicyOptions; trialFlowOrgs: string[] = [ @@ -115,6 +123,9 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { } private destroy$ = new Subject(); + protected enableTrialPayment$ = this.configService.getFeatureFlag$( + FeatureFlag.TrialPaymentOptional, + ); constructor( private route: ActivatedRoute, @@ -127,6 +138,8 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { private i18nService: I18nService, private routerService: RouterService, private acceptOrgInviteService: AcceptOrganizationInviteService, + private organizationBillingService: OrganizationBillingService, + private configService: ConfigService, ) {} async ngOnInit(): Promise { @@ -215,6 +228,30 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { } } + async createOrganizationOnTrial() { + this.loading = true; + const organization: OrganizationInformation = { + name: this.orgInfoFormGroup.get("name").value, + billingEmail: this.orgInfoFormGroup.get("email").value, + initiationPath: "Password Manager trial from marketing website", + }; + + const plan: PlanInformation = { + type: this.plan, + passwordManagerSeats: 1, + }; + + const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({ + organization, + plan, + }); + + this.orgId = response?.id; + this.billingSubLabel = `${this.i18nService.t("annual")} ($0/${this.i18nService.t("yr")})`; + this.loading = false; + this.verticalStepper.next(); + } + createdAccount(email: string) { this.email = email; this.orgInfoFormGroup.get("email")?.setValue(email); diff --git a/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts b/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts index a915d8f8a6c..0f6baa5f322 100644 --- a/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts +++ b/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts @@ -4,22 +4,11 @@ import { ActivatedRouteSnapshot, CanActivateFn } from "@angular/router"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderStatusType } from "@bitwarden/common/admin-console/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; export const organizationIsUnmanaged: CanActivateFn = async (route: ActivatedRouteSnapshot) => { - const configService = inject(ConfigService); const organizationService = inject(OrganizationService); const providerService = inject(ProviderService); - const consolidatedBillingEnabled = await configService.getFeatureFlag( - FeatureFlag.EnableConsolidatedBilling, - ); - - if (!consolidatedBillingEnabled) { - return true; - } - const organization = await organizationService.get(route.params.organizationId); if (!organization.hasProvider) { diff --git a/apps/web/src/app/billing/individual/user-subscription.component.html b/apps/web/src/app/billing/individual/user-subscription.component.html index eeb64ffe77d..1c1382cd816 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.html +++ b/apps/web/src/app/billing/individual/user-subscription.component.html @@ -48,16 +48,7 @@ }}
    {{ "nextCharge" | i18n }}
    -
    - {{ - nextInvoice - ? (nextInvoice.date | date: "mediumDate") + - ", " + - (nextInvoice.amount | currency: "$") - : "-" - }} -
    -
    +
    {{ nextInvoice ? (sub.subscription.periodEndDate | date: "mediumDate") + diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index e04b7c8b019..942767946ba 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -38,13 +38,9 @@ export class UserSubscriptionComponent implements OnInit { sub: SubscriptionResponse; selfHosted = false; cloudWebVaultUrl: string; - enableTimeThreshold: boolean; cancelPromise: Promise; reinstatePromise: Promise; - protected enableTimeThreshold$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableTimeThreshold, - ); protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( FeatureFlag.AC2476_DeprecateStripeSourcesAPI, @@ -69,7 +65,6 @@ export class UserSubscriptionComponent implements OnInit { async ngOnInit() { this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); await this.load(); - this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$); this.firstLoaded = true; } diff --git a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html index 4857a43a1ca..a09000ef55f 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html +++ b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html @@ -1,8 +1,8 @@ -

    + {{ (hasBillingToken ? "viewBillingToken" : "generateBillingToken") | i18n }} -

    +
    diff --git a/apps/web/src/app/billing/organizations/billing-sync-key.component.html b/apps/web/src/app/billing/organizations/billing-sync-key.component.html index 5f6b8482875..9736351deca 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-key.component.html +++ b/apps/web/src/app/billing/organizations/billing-sync-key.component.html @@ -1,8 +1,8 @@ -

    + {{ "manageBillingTokenSync" | i18n }} -

    +

    {{ "billingSyncKeyDesc" | i18n }}

    diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index e6ed6475c4a..878672a1fb9 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -345,16 +345,22 @@

    diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 0ba4829c7c8..5a6ac8c896a 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -282,6 +282,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { : this.discountPercentageFromSub + this.discountPercentage; } + isPaymentSourceEmpty() { + return this.deprecateStripeSourcesAPI + ? this.paymentSource === null || this.paymentSource === undefined + : this.billing?.paymentSource === null || this.billing?.paymentSource === undefined; + } + isSecretsManagerTrial(): boolean { return ( this.sub?.subscription?.items?.some((item) => @@ -723,7 +729,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { // Secrets Manager this.buildSecretsManagerRequest(request); - if (this.upgradeRequiresPaymentMethod || this.showPayment) { + if (this.upgradeRequiresPaymentMethod || this.showPayment || this.isPaymentSourceEmpty()) { if (this.deprecateStripeSourcesAPI) { const tokenizedPaymentSource = await this.paymentV2Component.tokenize(); const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index ccfe12b2e59..b25cda662f2 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from "@angular/core"; +import { BannerModule } from "../../../../../../libs/components/src/banner/banner.module"; import { UserVerificationModule } from "../../auth/shared/components/user-verification"; import { LooseComponentsModule } from "../../shared"; import { BillingSharedModule } from "../shared"; @@ -28,6 +29,7 @@ import { SubscriptionStatusComponent } from "./subscription-status.component"; BillingSharedModule, OrganizationPlansComponent, LooseComponentsModule, + BannerModule, ], declarations: [ AdjustSubscription, diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html index 16c5259e8ac..e1b74abea71 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -489,5 +489,6 @@ > {{ "cancel" | i18n }} + diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 68090b50e55..0cd21d0f688 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -48,10 +48,7 @@
    {{ "subscriptionExpiration" | i18n }}
    -
    - {{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }} -
    -
    +
    {{ nextInvoice ? (sub.subscription.periodEndDate | date: "mediumDate") : "-" }}
    @@ -264,7 +261,12 @@ - +

    {{ "manageSubscription" | i18n }}

    {{ "manageSubscriptionFromThe" | i18n }} @@ -281,7 +283,7 @@ - +

    {{ "billingManagedByProvider" | i18n: userOrg.providerName }}

    @@ -303,7 +305,13 @@ -
    diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 591371db081..f5cc89c86b6 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -5,8 +5,7 @@ import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUnti import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { OrganizationApiKeyType, ProviderStatusType } from "@bitwarden/common/admin-console/enums"; +import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; @@ -53,23 +52,14 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy loading = true; locale: string; showUpdatedSubscriptionStatusSection$: Observable; - enableTimeThreshold: boolean; preSelectedProductTier: ProductTierType = ProductTierType.Free; showSubscription = true; showSelfHost = false; - providerIsOnConsolidatedBilling = false; + organizationIsManagedByConsolidatedBillingMSP = false; protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon; protected readonly teamsStarter = ProductTierType.TeamsStarter; - protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableConsolidatedBilling, - ); - - protected enableTimeThreshold$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableTimeThreshold, - ); - protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$( FeatureFlag.EnableUpgradePasswordManagerSub, ); @@ -91,7 +81,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy private configService: ConfigService, private toastService: ToastService, private billingApiService: BillingApiServiceAbstraction, - private providerService: ProviderService, ) {} async ngOnInit() { @@ -119,7 +108,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$( FeatureFlag.AC1795_UpdatedSubscriptionStatusSection, ); - this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$); } ngOnDestroy() { @@ -132,25 +120,22 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.locale = await firstValueFrom(this.i18nService.locale$); this.userOrg = await this.organizationService.get(this.organizationId); - const consolidatedBillingEnabled = await firstValueFrom(this.enableConsolidatedBilling$); - - const provider = this.userOrg.hasProvider - ? await this.providerService.get(this.userOrg.providerId) - : null; - - this.providerIsOnConsolidatedBilling = - consolidatedBillingEnabled && provider?.providerStatus === ProviderStatusType.Billable; - const isIndependentOrganizationOwner = !this.userOrg.hasProvider && this.userOrg.isOwner; - const isProviderUser = this.userOrg.hasProvider && this.userOrg.isProviderUser; - - this.showSubscription = - isIndependentOrganizationOwner || (isProviderUser && !this.providerIsOnConsolidatedBilling); + const isResoldOrganizationOwner = this.userOrg.hasReseller && this.userOrg.isOwner; + const isMSPUser = this.userOrg.hasProvider && this.userOrg.isProviderUser; const metadata = await this.billingApiService.getOrganizationBillingMetadata( this.organizationId, ); + this.organizationIsManagedByConsolidatedBillingMSP = + this.userOrg.hasProvider && metadata.isManaged; + + this.showSubscription = + isIndependentOrganizationOwner || + isResoldOrganizationOwner || + (isMSPUser && !this.organizationIsManagedByConsolidatedBillingMSP); + this.showSelfHost = metadata.isEligibleForSelfHost; if (this.showSubscription) { @@ -301,9 +286,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy return this.i18nService.t("subscriptionUpgrade", this.sub.seats.toString()); } } else if (this.sub.maxAutoscaleSeats === this.sub.seats && this.sub.seats != null) { - if (!this.enableTimeThreshold) { - return this.i18nService.t("subscriptionMaxReached", this.sub.seats.toString()); - } const seatAdjustmentMessage = this.sub.plan.isAnnual ? "annualSubscriptionUserSeatsMessage" : "monthlySubscriptionUserSeatsMessage"; @@ -314,21 +296,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy } else if (this.userOrg.productTierType === ProductTierType.TeamsStarter) { return this.i18nService.t("subscriptionUserSeatsWithoutAdditionalSeatsOption", 10); } else if (this.sub.maxAutoscaleSeats == null) { - if (!this.enableTimeThreshold) { - return this.i18nService.t("subscriptionUserSeatsUnlimitedAutoscale"); - } - const seatAdjustmentMessage = this.sub.plan.isAnnual ? "annualSubscriptionUserSeatsMessage" : "monthlySubscriptionUserSeatsMessage"; return this.i18nService.t(seatAdjustmentMessage); } else { - if (!this.enableTimeThreshold) { - return this.i18nService.t( - "subscriptionUserSeatsLimitedAutoscale", - this.sub.maxAutoscaleSeats.toString(), - ); - } const seatAdjustmentMessage = this.sub.plan.isAnnual ? "annualSubscriptionUserSeatsMessage" : "monthlySubscriptionUserSeatsMessage"; @@ -525,6 +497,10 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy get showChangePlanButton() { return this.sub.plan.productTier !== ProductTierType.Enterprise && !this.showChangePlan; } + + get canUseBillingSync() { + return this.userOrg.productTierType === ProductTierType.Enterprise; + } } /** diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html index 9f9cb9efc65..7a6e8558bae 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html @@ -1,3 +1,22 @@ + + {{ freeTrialData.message }} + + {{ "routeToPaymentMethodTrigger" | i18n }} + + diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index 0756a6c314c..e2178e7c02c 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -1,17 +1,25 @@ -import { Component, ViewChild } from "@angular/core"; +import { Location } from "@angular/common"; +import { Component, OnDestroy, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; import { from, lastValueFrom, switchMap } from "rxjs"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; +import { FreeTrial } from "../../../core/types/free-trial"; +import { TrialFlowService } from "../../services/trial-flow.service"; import { TaxInfoComponent } from "../../shared"; import { AddCreditDialogResult, @@ -25,26 +33,36 @@ import { @Component({ templateUrl: "./organization-payment-method.component.html", }) -export class OrganizationPaymentMethodComponent { +export class OrganizationPaymentMethodComponent implements OnDestroy { @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; organizationId: string; + isUnpaid = false; accountCredit: number; paymentSource?: PaymentSourceResponse; subscriptionStatus?: string; + protected freeTrialData: FreeTrial; + organization: Organization; + organizationSubscriptionResponse: OrganizationSubscriptionResponse; loading = true; protected readonly Math = Math; + launchPaymentModalAutomatically = false; constructor( private activatedRoute: ActivatedRoute, private billingApiService: BillingApiServiceAbstraction, + protected organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private router: Router, private toastService: ToastService, + private location: Location, + private trialFlowService: TrialFlowService, + private organizationService: OrganizationService, + protected syncService: SyncService, ) { this.activatedRoute.params .pipe( @@ -59,6 +77,23 @@ export class OrganizationPaymentMethodComponent { }), ) .subscribe(); + + const state = this.router.getCurrentNavigation()?.extras?.state; + // incase the above state is undefined or null we use redundantState + const redundantState: any = location.getState(); + if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) { + this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically; + } else if ( + redundantState && + Object.prototype.hasOwnProperty.call(redundantState, "launchPaymentModalAutomatically") + ) { + this.launchPaymentModalAutomatically = redundantState.launchPaymentModalAutomatically; + } else { + this.launchPaymentModalAutomatically = false; + } + } + ngOnDestroy(): void { + this.launchPaymentModalAutomatically = false; } protected addAccountCredit = async (): Promise => { @@ -82,6 +117,34 @@ export class OrganizationPaymentMethodComponent { this.accountCredit = accountCredit; this.paymentSource = paymentSource; this.subscriptionStatus = subscriptionStatus; + + if (this.organizationId) { + const organizationSubscriptionPromise = this.organizationApiService.getSubscription( + this.organizationId, + ); + const organizationPromise = this.organizationService.get(this.organizationId); + + [this.organizationSubscriptionResponse, this.organization] = await Promise.all([ + organizationSubscriptionPromise, + organizationPromise, + ]); + this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( + this.organization, + this.organizationSubscriptionResponse, + paymentSource, + ); + } + this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false; + // If the flag `launchPaymentModalAutomatically` is set to true, + // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. + // This delay ensures that any prior UI/rendering operations complete before triggering the modal. + if (this.launchPaymentModalAutomatically) { + window.setTimeout(async () => { + await this.changePayment(); + this.launchPaymentModalAutomatically = false; + this.location.replaceState(this.location.path(), "", {}); + }, 800); + } this.loading = false; }; @@ -100,6 +163,24 @@ export class OrganizationPaymentMethodComponent { } }; + changePayment = async () => { + const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, { + data: { + initialPaymentMethod: this.paymentSource?.type, + organizationId: this.organizationId, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustPaymentDialogV2ResultType.Submitted) { + this.location.replaceState(this.location.path(), "", {}); + if (this.launchPaymentModalAutomatically && !this.organization.enabled) { + await this.syncService.fullSync(true); + } + this.launchPaymentModalAutomatically = false; + await this.load(); + } + }; + protected updateTaxInformation = async (): Promise => { this.taxInfoComponent.taxFormGroup.updateValueAndValidity(); this.taxInfoComponent.taxFormGroup.markAllAsTouched(); diff --git a/apps/web/src/app/billing/services/trial-flow.service.ts b/apps/web/src/app/billing/services/trial-flow.service.ts new file mode 100644 index 00000000000..3135a811665 --- /dev/null +++ b/apps/web/src/app/billing/services/trial-flow.service.ts @@ -0,0 +1,100 @@ +import { Injectable } from "@angular/core"; +import { Router } from "@angular/router"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; +import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; +import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService } from "@bitwarden/components"; + +import { FreeTrial } from "../../core/types/free-trial"; + +@Injectable({ providedIn: "root" }) +export class TrialFlowService { + constructor( + private i18nService: I18nService, + protected dialogService: DialogService, + private router: Router, + protected billingApiService: BillingApiServiceAbstraction, + ) {} + checkForOrgsWithUpcomingPaymentIssues( + organization: Organization, + organizationSubscription: OrganizationSubscriptionResponse, + paymentSource: BillingSourceResponse | PaymentSourceResponse, + ): FreeTrial { + const trialEndDate = organizationSubscription?.subscription?.trialEndDate; + const displayBanner = + !paymentSource && + organization?.isOwner && + organizationSubscription?.subscription?.status === "trialing"; + const trialRemainingDays = trialEndDate ? this.calculateTrialRemainingDays(trialEndDate) : 0; + const freeTrialMessage = this.getFreeTrialMessage(trialRemainingDays); + + return { + remainingDays: trialRemainingDays, + message: freeTrialMessage, + shownBanner: displayBanner, + organizationId: organization.id, + organizationName: organization.name, + }; + } + + calculateTrialRemainingDays(trialEndDate: string): number | undefined { + const today = new Date(); + const trialEnd = new Date(trialEndDate); + const timeDifference = trialEnd.getTime() - today.getTime(); + + return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); + } + + getFreeTrialMessage(trialRemainingDays: number): string { + if (trialRemainingDays >= 2) { + return this.i18nService.t("freeTrialEndPrompt", trialRemainingDays); + } else if (trialRemainingDays === 1) { + return this.i18nService.t("freeTrialEndPromptForOneDayNoOrgName"); + } else { + return this.i18nService.t("freeTrialEndingSoonWithoutOrgName"); + } + } + + async handleUnpaidSubscriptionDialog( + org: Organization, + organizationBillingMetadata: OrganizationBillingMetadataResponse, + ): Promise { + if (organizationBillingMetadata.isSubscriptionUnpaid) { + const confirmed = await this.promptForPaymentNavigation(org); + if (confirmed) { + await this.navigateToPaymentMethod(org?.id); + } + } + } + + private async promptForPaymentNavigation(org: Organization): Promise { + if (!org?.isOwner) { + await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org?.name), + content: { key: "suspendedUserOrgMessage" }, + type: "danger", + acceptButtonText: this.i18nService.t("close"), + cancelButtonText: null, + }); + return false; + } + return await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org?.name), + content: { key: "suspendedOwnerOrgMessage" }, + type: "danger", + acceptButtonText: this.i18nService.t("continue"), + cancelButtonText: this.i18nService.t("close"), + }); + } + + private async navigateToPaymentMethod(orgId: string) { + await this.router.navigate(["organizations", `${orgId}`, "billing", "payment-method"], { + state: { launchPaymentModalAutomatically: true }, + }); + } +} diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts index 450c1234567..0c8e93531ee 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts @@ -74,6 +74,7 @@ export class AdjustPaymentDialogComponent { } }); await response; + await new Promise((resolve) => setTimeout(resolve, 10000)); this.toastService.showToast({ variant: "success", title: null, diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index 57491a73e6d..b9c235943ad 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -1,5 +1,7 @@ import { NgModule } from "@angular/core"; +import { BannerModule } from "@bitwarden/components"; + import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; @@ -27,6 +29,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac PaymentComponent, TaxInfoComponent, HeaderModule, + BannerModule, PaymentV2Component, VerifyBankAccountComponent, ], diff --git a/apps/web/src/app/billing/shared/payment-method.component.html b/apps/web/src/app/billing/shared/payment-method.component.html index 495785af45f..1d4675847a1 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.html +++ b/apps/web/src/app/billing/shared/payment-method.component.html @@ -1,3 +1,23 @@ + + {{ freeTrialData?.message }} + + {{ "routeToPaymentMethodTrigger" | i18n }} + + +

    diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index 06acf2142a5..98e6efcd8bd 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -1,10 +1,13 @@ -import { Component, OnInit, ViewChild } from "@angular/core"; +import { Location } from "@angular/common"; +import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { lastValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BillingPaymentResponse } from "@bitwarden/common/billing/models/response/billing-payment.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -13,8 +16,12 @@ import { VerifyBankRequest } from "@bitwarden/common/models/request/verify-bank. import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; +import { FreeTrial } from "../../core/types/free-trial"; +import { TrialFlowService } from "../services/trial-flow.service"; + import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component"; import { AdjustPaymentDialogResult, @@ -26,7 +33,7 @@ import { TaxInfoComponent } from "./tax-info.component"; templateUrl: "payment-method.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class PaymentMethodComponent implements OnInit { +export class PaymentMethodComponent implements OnInit, OnDestroy { @ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent; loading = false; @@ -37,6 +44,7 @@ export class PaymentMethodComponent implements OnInit { paymentMethodType = PaymentMethodType; organizationId: string; isUnpaid = false; + organization: Organization; verifyBankForm = this.formBuilder.group({ amount1: new FormControl(null, [ @@ -52,6 +60,8 @@ export class PaymentMethodComponent implements OnInit { }); taxForm = this.formBuilder.group({}); + launchPaymentModalAutomatically = false; + protected freeTrialData: FreeTrial; constructor( protected apiService: ApiService, @@ -59,12 +69,30 @@ export class PaymentMethodComponent implements OnInit { protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, private router: Router, + private location: Location, private logService: LogService, private route: ActivatedRoute, private formBuilder: FormBuilder, private dialogService: DialogService, private toastService: ToastService, - ) {} + private trialFlowService: TrialFlowService, + private organizationService: OrganizationService, + protected syncService: SyncService, + ) { + const state = this.router.getCurrentNavigation()?.extras?.state; + // incase the above state is undefined or null we use redundantState + const redundantState: any = location.getState(); + if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) { + this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically; + } else if ( + redundantState && + Object.prototype.hasOwnProperty.call(redundantState, "launchPaymentModalAutomatically") + ) { + this.launchPaymentModalAutomatically = redundantState.launchPaymentModalAutomatically; + } else { + this.launchPaymentModalAutomatically = false; + } + } async ngOnInit() { // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe @@ -88,27 +116,37 @@ export class PaymentMethodComponent implements OnInit { return; } this.loading = true; - if (this.forOrganization) { const billingPromise = this.organizationApiService.getBilling(this.organizationId); const organizationSubscriptionPromise = this.organizationApiService.getSubscription( this.organizationId, ); + const organizationPromise = this.organizationService.get(this.organizationId); - [this.billing, this.org] = await Promise.all([ + [this.billing, this.org, this.organization] = await Promise.all([ billingPromise, organizationSubscriptionPromise, + organizationPromise, ]); + this.determineOrgsWithUpcomingPaymentIssues(); } else { const billingPromise = this.apiService.getUserBillingPayment(); const subPromise = this.apiService.getUserSubscription(); [this.billing, this.sub] = await Promise.all([billingPromise, subPromise]); } - this.isUnpaid = this.subscription?.status === "unpaid" ?? false; - this.loading = false; + // If the flag `launchPaymentModalAutomatically` is set to true, + // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. + // This delay ensures that any prior UI/rendering operations complete before triggering the modal. + if (this.launchPaymentModalAutomatically) { + window.setTimeout(async () => { + await this.changePayment(); + this.launchPaymentModalAutomatically = false; + this.location.replaceState(this.location.path(), "", {}); + }, 800); + } }; addCredit = async () => { @@ -132,6 +170,11 @@ export class PaymentMethodComponent implements OnInit { }); const result = await lastValueFrom(dialogRef.closed); if (result === AdjustPaymentDialogResult.Adjusted) { + this.location.replaceState(this.location.path(), "", {}); + if (this.launchPaymentModalAutomatically && !this.organization.enabled) { + await this.syncService.fullSync(true); + } + this.launchPaymentModalAutomatically = false; await this.load(); } }; @@ -162,6 +205,14 @@ export class PaymentMethodComponent implements OnInit { }); }; + determineOrgsWithUpcomingPaymentIssues() { + this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( + this.organization, + this.org, + this.billing?.paymentSource, + ); + } + get isCreditBalance() { return this.billing == null || this.billing.balance <= 0; } @@ -203,4 +254,8 @@ export class PaymentMethodComponent implements OnInit { get subscription() { return this.sub?.subscription ?? this.org?.subscription ?? null; } + + ngOnDestroy(): void { + this.launchPaymentModalAutomatically = false; + } } diff --git a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts b/apps/web/src/app/billing/shared/payment/payment-v2.component.ts index 6c12b14cf0d..c5ce4eac400 100644 --- a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts +++ b/apps/web/src/app/billing/shared/payment/payment-v2.component.ts @@ -93,7 +93,7 @@ export class PaymentV2Component implements OnInit, OnDestroy { protected submit = async () => { const { type, token } = await this.tokenize(); - await this.onSubmit({ type, token }); + await this.onSubmit?.({ type, token }); this.submitted.emit(type); }; diff --git a/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.ts b/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.ts index 5a6a7b0cf3d..2f9e34f877b 100644 --- a/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.ts +++ b/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.ts @@ -35,7 +35,7 @@ export class VerifyBankAccountComponent { this.formGroup.value.amount1, this.formGroup.value.amount2, ); - await this.onSubmit(request); + await this.onSubmit?.(request); this.submitted.emit(); }; } diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 0bfe1d0dc89..be6a62443d3 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -330,6 +330,20 @@ export class EventService { this.getShortId(ev.organizationUserId), ); break; + case EventType.OrganizationUser_Deleted: + msg = this.i18nService.t("deletedUserId", this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t( + "deletedUserId", + this.getShortId(ev.organizationUserId), + ); + break; + case EventType.OrganizationUser_Left: + msg = this.i18nService.t("userLeftOrganization", this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t( + "userLeftOrganization", + this.getShortId(ev.organizationUserId), + ); + break; // Org case EventType.Organization_Updated: msg = humanReadableMsg = this.i18nService.t("editedOrgSettings"); diff --git a/apps/web/src/app/core/types/free-trial.ts b/apps/web/src/app/core/types/free-trial.ts new file mode 100644 index 00000000000..ee5fb921621 --- /dev/null +++ b/apps/web/src/app/core/types/free-trial.ts @@ -0,0 +1,7 @@ +export type FreeTrial = { + remainingDays: number; + message: string; + shownBanner: boolean; + organizationId: string; + organizationName: string; +}; diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index e4e5ab2caaa..6c2f15a02a6 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -1,7 +1,7 @@ import { Injectable } from "@angular/core"; import { firstValueFrom } from "rxjs"; -import { AccountInfo } from "@bitwarden/common/auth/abstractions/account.service"; +import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; @@ -46,10 +46,7 @@ export class UserKeyRotationService { * Creates a new user key and re-encrypts all required data with the it. * @param masterPassword current master password (used for validation) */ - async rotateUserKeyAndEncryptedData( - masterPassword: string, - user: { id: UserId } & AccountInfo, - ): Promise { + async rotateUserKeyAndEncryptedData(masterPassword: string, user: Account): Promise { this.logService.info("[Userkey rotation] Starting user key rotation..."); if (!masterPassword) { this.logService.info("[Userkey rotation] Invalid master password provided. Aborting!"); diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html index 1dd03d03230..f34d32f5983 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html @@ -22,6 +22,7 @@ [route]="['../', org.id]" (mainContentClicked)="toggle()" [routerLinkActiveOptions]="{ exact: true }" + (click)="handleUnpaidSubscription(org)" > - setTimeout(() => { - this.dataLastUpdated = new Date(); - resolve(true); - }, 1000), - ); - } - - constructor(route: ActivatedRoute) { - route.queryParams.pipe(takeUntilDestroyed(), first()).subscribe(({ tabIndex }) => { - this.tabIndex = !isNaN(tabIndex) ? tabIndex : AccessIntelligenceTabType.AllApps; - }); - } -} diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence.module.ts b/apps/web/src/app/tools/access-intelligence/access-intelligence.module.ts deleted file mode 100644 index 32b66935b69..00000000000 --- a/apps/web/src/app/tools/access-intelligence/access-intelligence.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NgModule } from "@angular/core"; - -import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module"; -import { AccessIntelligenceComponent } from "./access-intelligence.component"; - -@NgModule({ - imports: [AccessIntelligenceComponent, AccessIntelligenceRoutingModule], -}) -export class AccessIntelligenceModule {} diff --git a/apps/web/src/app/tools/access-intelligence/application-table.component.html b/apps/web/src/app/tools/access-intelligence/application-table.component.html deleted file mode 100644 index 4986483cb75..00000000000 --- a/apps/web/src/app/tools/access-intelligence/application-table.component.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - {{ "application" | i18n }} - {{ "atRiskPasswords" | i18n }} - {{ "totalPasswords" | i18n }} - {{ "atRiskMembers" | i18n }} - {{ "totalMembers" | i18n }} - - - diff --git a/apps/web/src/app/tools/access-intelligence/application-table.component.ts b/apps/web/src/app/tools/access-intelligence/application-table.component.ts deleted file mode 100644 index 79b8500b8c7..00000000000 --- a/apps/web/src/app/tools/access-intelligence/application-table.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { TableDataSource, TableModule } from "@bitwarden/components"; - -@Component({ - standalone: true, - selector: "tools-application-table", - templateUrl: "./application-table.component.html", - imports: [CommonModule, JslibModule, TableModule], -}) -export class ApplicationTableComponent { - protected dataSource = new TableDataSource(); - - constructor() { - this.dataSource.data = []; - } -} diff --git a/apps/web/src/app/tools/access-intelligence/no-priority-apps.component.html b/apps/web/src/app/tools/access-intelligence/no-priority-apps.component.html deleted file mode 100644 index 9c3b4f48916..00000000000 --- a/apps/web/src/app/tools/access-intelligence/no-priority-apps.component.html +++ /dev/null @@ -1,15 +0,0 @@ - - -

    - {{ "noPriorityApplicationsTitle" | i18n }} -

    -
    - -

    - {{ "noPriorityApplicationsDescription" | i18n }} -

    -
    - - - - diff --git a/apps/web/src/app/tools/access-intelligence/no-priority-apps.component.ts b/apps/web/src/app/tools/access-intelligence/no-priority-apps.component.ts deleted file mode 100644 index e4e54ca2f13..00000000000 --- a/apps/web/src/app/tools/access-intelligence/no-priority-apps.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { ButtonModule, NoItemsModule, Icons } from "@bitwarden/components"; - -@Component({ - standalone: true, - selector: "tools-no-priority-apps", - templateUrl: "no-priority-apps.component.html", - imports: [ButtonModule, CommonModule, JslibModule, NoItemsModule], -}) -export class NoPriorityAppsComponent { - noItemsIcon = Icons.NoResults; -} diff --git a/apps/web/src/app/tools/risk-insights/all-applications.component.html b/apps/web/src/app/tools/risk-insights/all-applications.component.html new file mode 100644 index 00000000000..4ed31adea78 --- /dev/null +++ b/apps/web/src/app/tools/risk-insights/all-applications.component.html @@ -0,0 +1,114 @@ +
    + + {{ "loading" | i18n }} +
    +
    + + +

    + {{ "noAppsInOrgTitle" | i18n: organization.name }} +

    +
    + +
    + + {{ "noAppsInOrgDescription" | i18n }} + + {{ "learnMore" | i18n }} +
    +
    + + + +
    +
    +
    +

    {{ "allApplications" | i18n }}

    +
    + + + + +
    +
    + + +
    + + + + + {{ "application" | i18n }} + {{ "atRiskPasswords" | i18n }} + {{ "totalPasswords" | i18n }} + {{ "atRiskMembers" | i18n }} + {{ "totalMembers" | i18n }} + + + + + + + + + {{ r.name }} + + + + {{ r.atRiskPasswords }} + + + + + {{ r.totalPasswords }} + + + + + {{ r.atRiskMembers }} + + + + {{ r.totalMembers }} + + + + +
    diff --git a/apps/web/src/app/tools/risk-insights/all-applications.component.ts b/apps/web/src/app/tools/risk-insights/all-applications.component.ts new file mode 100644 index 00000000000..5d76403f46b --- /dev/null +++ b/apps/web/src/app/tools/risk-insights/all-applications.component.ts @@ -0,0 +1,126 @@ +import { Component, DestroyRef, inject, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; +import { debounceTime, firstValueFrom, map } from "rxjs"; + +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + Icons, + NoItemsModule, + SearchModule, + TableDataSource, + ToastService, +} from "@bitwarden/components"; +import { CardComponent } from "@bitwarden/tools-card"; + +import { HeaderModule } from "../../layouts/header/header.module"; +import { SharedModule } from "../../shared"; +import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; + +import { applicationTableMockData } from "./application-table.mock"; + +@Component({ + standalone: true, + selector: "tools-all-applications", + templateUrl: "./all-applications.component.html", + imports: [HeaderModule, CardComponent, SearchModule, PipesModule, NoItemsModule, SharedModule], +}) +export class AllApplicationsComponent implements OnInit { + protected dataSource = new TableDataSource(); + protected selectedIds: Set = new Set(); + protected searchControl = new FormControl("", { nonNullable: true }); + private destroyRef = inject(DestroyRef); + protected loading = false; + protected organization: Organization; + noItemsIcon = Icons.Security; + protected markingAsCritical = false; + isCritialAppsFeatureEnabled = false; + + // MOCK DATA + protected mockData = applicationTableMockData; + protected mockAtRiskMembersCount = 0; + protected mockAtRiskAppsCount = 0; + protected mockTotalMembersCount = 0; + protected mockTotalAppsCount = 0; + + async ngOnInit() { + this.activatedRoute.paramMap + .pipe( + takeUntilDestroyed(this.destroyRef), + map(async (params) => { + const organizationId = params.get("organizationId"); + this.organization = await firstValueFrom(this.organizationService.get$(organizationId)); + // TODO: use organizationId to fetch data + }), + ) + .subscribe(); + + this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.CriticalApps, + ); + } + + constructor( + protected cipherService: CipherService, + protected passwordStrengthService: PasswordStrengthServiceAbstraction, + protected auditService: AuditService, + protected i18nService: I18nService, + protected activatedRoute: ActivatedRoute, + protected toastService: ToastService, + protected organizationService: OrganizationService, + protected configService: ConfigService, + ) { + this.dataSource.data = applicationTableMockData; + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((v) => (this.dataSource.filter = v)); + } + + goToCreateNewLoginItem = async () => { + // TODO: implement + this.toastService.showToast({ + variant: "warning", + title: null, + message: "Not yet implemented", + }); + }; + + markAppsAsCritical = async () => { + // TODO: Send to API once implemented + this.markingAsCritical = true; + return new Promise((resolve) => { + setTimeout(() => { + this.selectedIds.clear(); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("appsMarkedAsCritical"), + }); + resolve(true); + this.markingAsCritical = false; + }, 1000); + }); + }; + + trackByFunction(_: number, item: CipherView) { + return item.id; + } + + onCheckboxChange(id: number, event: Event) { + const isChecked = (event.target as HTMLInputElement).checked; + if (isChecked) { + this.selectedIds.add(id); + } else { + this.selectedIds.delete(id); + } + } +} diff --git a/apps/web/src/app/tools/risk-insights/application-table.mock.ts b/apps/web/src/app/tools/risk-insights/application-table.mock.ts new file mode 100644 index 00000000000..4df363ab2c7 --- /dev/null +++ b/apps/web/src/app/tools/risk-insights/application-table.mock.ts @@ -0,0 +1,50 @@ +export const applicationTableMockData = [ + { + id: 1, + name: "google.com", + atRiskPasswords: 4, + totalPasswords: 10, + atRiskMembers: 2, + totalMembers: 5, + }, + { + id: 2, + name: "facebook.com", + atRiskPasswords: 3, + totalPasswords: 8, + atRiskMembers: 1, + totalMembers: 3, + }, + { + id: 3, + name: "twitter.com", + atRiskPasswords: 2, + totalPasswords: 6, + atRiskMembers: 0, + totalMembers: 2, + }, + { + id: 4, + name: "linkedin.com", + atRiskPasswords: 1, + totalPasswords: 4, + atRiskMembers: 0, + totalMembers: 1, + }, + { + id: 5, + name: "instagram.com", + atRiskPasswords: 0, + totalPasswords: 2, + atRiskMembers: 0, + totalMembers: 0, + }, + { + id: 6, + name: "tiktok.com", + atRiskPasswords: 0, + totalPasswords: 1, + atRiskMembers: 0, + totalMembers: 0, + }, +]; diff --git a/apps/web/src/app/tools/risk-insights/critical-applications.component.html b/apps/web/src/app/tools/risk-insights/critical-applications.component.html new file mode 100644 index 00000000000..1c503f3d786 --- /dev/null +++ b/apps/web/src/app/tools/risk-insights/critical-applications.component.html @@ -0,0 +1,99 @@ +
    + + {{ "loading" | i18n }} +
    +
    + + +

    + {{ "noCriticalAppsTitle" | i18n }} +

    +
    + +

    + {{ "noCriticalAppsDescription" | i18n }} +

    +
    + + + +
    +
    +
    +
    +

    {{ "criticalApplications" | i18n }}

    + +
    +
    + + + + +
    +
    + +
    + + + + + {{ "application" | i18n }} + {{ "atRiskPasswords" | i18n }} + {{ "totalPasswords" | i18n }} + {{ "atRiskMembers" | i18n }} + {{ "totalMembers" | i18n }} + + + + + + + + + {{ r.name }} + + + + {{ r.atRiskPasswords }} + + + + + {{ r.totalPasswords }} + + + + + {{ r.atRiskMembers }} + + + + {{ r.totalMembers }} + + + + +
    diff --git a/apps/web/src/app/tools/risk-insights/critical-applications.component.ts b/apps/web/src/app/tools/risk-insights/critical-applications.component.ts new file mode 100644 index 00000000000..0779b2977e5 --- /dev/null +++ b/apps/web/src/app/tools/risk-insights/critical-applications.component.ts @@ -0,0 +1,68 @@ +import { Component, DestroyRef, inject, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; +import { ActivatedRoute, Router } from "@angular/router"; +import { debounceTime, map } from "rxjs"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SearchModule, TableDataSource, NoItemsModule, Icons } from "@bitwarden/components"; +import { CardComponent } from "@bitwarden/tools-card"; + +import { HeaderModule } from "../../layouts/header/header.module"; +import { SharedModule } from "../../shared"; +import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; + +import { applicationTableMockData } from "./application-table.mock"; +import { RiskInsightsTabType } from "./risk-insights.component"; + +@Component({ + standalone: true, + selector: "tools-critical-applications", + templateUrl: "./critical-applications.component.html", + imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule], +}) +export class CriticalApplicationsComponent implements OnInit { + protected dataSource = new TableDataSource(); + protected selectedIds: Set = new Set(); + protected searchControl = new FormControl("", { nonNullable: true }); + private destroyRef = inject(DestroyRef); + protected loading = false; + protected organizationId: string; + noItemsIcon = Icons.Security; + // MOCK DATA + protected mockData = applicationTableMockData; + protected mockAtRiskMembersCount = 0; + protected mockAtRiskAppsCount = 0; + protected mockTotalMembersCount = 0; + protected mockTotalAppsCount = 0; + + ngOnInit() { + this.activatedRoute.paramMap + .pipe( + takeUntilDestroyed(this.destroyRef), + map(async (params) => { + this.organizationId = params.get("organizationId"); + // TODO: use organizationId to fetch data + }), + ) + .subscribe(); + } + + goToAllAppsTab = async () => { + await this.router.navigate([`organizations/${this.organizationId}/risk-insights`], { + queryParams: { tabIndex: RiskInsightsTabType.AllApps }, + queryParamsHandling: "merge", + }); + }; + + constructor( + protected i18nService: I18nService, + protected activatedRoute: ActivatedRoute, + protected router: Router, + ) { + this.dataSource.data = []; //applicationTableMockData; + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((v) => (this.dataSource.filter = v)); + } +} diff --git a/apps/web/src/app/tools/access-intelligence/notified-members-table.component.html b/apps/web/src/app/tools/risk-insights/notified-members-table.component.html similarity index 100% rename from apps/web/src/app/tools/access-intelligence/notified-members-table.component.html rename to apps/web/src/app/tools/risk-insights/notified-members-table.component.html diff --git a/apps/web/src/app/tools/access-intelligence/notified-members-table.component.ts b/apps/web/src/app/tools/risk-insights/notified-members-table.component.ts similarity index 100% rename from apps/web/src/app/tools/access-intelligence/notified-members-table.component.ts rename to apps/web/src/app/tools/risk-insights/notified-members-table.component.ts diff --git a/apps/web/src/app/tools/access-intelligence/password-health-members.component.html b/apps/web/src/app/tools/risk-insights/password-health-members-uri.component.html similarity index 60% rename from apps/web/src/app/tools/access-intelligence/password-health-members.component.html rename to apps/web/src/app/tools/risk-insights/password-health-members-uri.component.html index 885c21f0a2c..bdccc523e76 100644 --- a/apps/web/src/app/tools/access-intelligence/password-health-members.component.html +++ b/apps/web/src/app/tools/risk-insights/password-health-members-uri.component.html @@ -8,38 +8,11 @@ >
    {{ "loading" | i18n }}

    -
    - -
    -
    -
    - - - - -
    -
    - - -
    +
    - - {{ "name" | i18n }} + {{ "application" | i18n }} {{ "weakness" | i18n }} {{ "timesReused" | i18n }} {{ "timesExposed" | i18n }} @@ -48,15 +21,10 @@ - - - - {{ r.name }} + {{ r.hostURI }} -
    - {{ r.subTitle }} { + let component: PasswordHealthMembersURIComponent; + let fixture: ComponentFixture; + let cipherServiceMock: MockProxy; + const passwordHealthServiceMock = mock(); + + const activeRouteParams = convertToParamMap({ organizationId: "orgId" }); + + beforeEach(async () => { + cipherServiceMock = mock(); + await TestBed.configureTestingModule({ + imports: [PasswordHealthMembersURIComponent, PipesModule, TableModule, LooseComponentsModule], + providers: [ + { provide: CipherService, useValue: cipherServiceMock }, + { provide: I18nService, useValue: mock() }, + { provide: AuditService, useValue: mock() }, + { provide: OrganizationService, useValue: mock() }, + { + provide: PasswordStrengthServiceAbstraction, + useValue: mock(), + }, + { provide: PasswordHealthService, useValue: passwordHealthServiceMock }, + { + provide: ActivatedRoute, + useValue: { + paramMap: of(activeRouteParams), + url: of([]), + }, + }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordHealthMembersURIComponent); + component = fixture.componentInstance; + }); + + it("should initialize component", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/web/src/app/tools/risk-insights/password-health-members-uri.component.ts b/apps/web/src/app/tools/risk-insights/password-health-members-uri.component.ts new file mode 100644 index 00000000000..c977c829537 --- /dev/null +++ b/apps/web/src/app/tools/risk-insights/password-health-members-uri.component.ts @@ -0,0 +1,108 @@ +import { CommonModule } from "@angular/common"; +import { Component, DestroyRef, inject, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute } from "@angular/router"; +import { map } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +// eslint-disable-next-line no-restricted-imports +import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + BadgeModule, + BadgeVariant, + ContainerComponent, + TableDataSource, + TableModule, +} from "@bitwarden/components"; + +// eslint-disable-next-line no-restricted-imports +import { HeaderModule } from "../../layouts/header/header.module"; +// eslint-disable-next-line no-restricted-imports +import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module"; +// eslint-disable-next-line no-restricted-imports +import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; + +@Component({ + standalone: true, + selector: "tools-password-health-members-uri", + templateUrl: "password-health-members-uri.component.html", + imports: [ + BadgeModule, + OrganizationBadgeModule, + CommonModule, + ContainerComponent, + PipesModule, + JslibModule, + HeaderModule, + TableModule, + ], + providers: [PasswordHealthService], +}) +export class PasswordHealthMembersURIComponent implements OnInit { + passwordStrengthMap = new Map(); + + weakPasswordCiphers: CipherView[] = []; + + passwordUseMap = new Map(); + + exposedPasswordMap = new Map(); + + totalMembersMap = new Map(); + + dataSource = new TableDataSource(); + + reportCiphers: (CipherView & { hostURI: string })[] = []; + reportCipherURIs: string[] = []; + + organization: Organization; + + loading = true; + + private destroyRef = inject(DestroyRef); + + constructor( + protected cipherService: CipherService, + protected passwordStrengthService: PasswordStrengthServiceAbstraction, + protected organizationService: OrganizationService, + protected auditService: AuditService, + protected i18nService: I18nService, + protected activatedRoute: ActivatedRoute, + ) {} + + ngOnInit() { + this.activatedRoute.paramMap + .pipe( + takeUntilDestroyed(this.destroyRef), + map(async (params) => { + const organizationId = params.get("organizationId"); + await this.setCiphers(organizationId); + }), + ) + .subscribe(); + } + + async setCiphers(organizationId: string) { + const passwordHealthService = new PasswordHealthService( + this.passwordStrengthService, + this.auditService, + this.cipherService, + organizationId, + ); + + await passwordHealthService.generateReport(); + + this.dataSource.data = passwordHealthService.groupCiphersByLoginUri(); + this.exposedPasswordMap = passwordHealthService.exposedPasswordMap; + this.passwordStrengthMap = passwordHealthService.passwordStrengthMap; + this.passwordUseMap = passwordHealthService.passwordUseMap; + this.totalMembersMap = passwordHealthService.totalMembersMap; + this.loading = false; + } +} diff --git a/apps/web/src/app/tools/risk-insights/password-health-members.component.html b/apps/web/src/app/tools/risk-insights/password-health-members.component.html new file mode 100644 index 00000000000..7f9b37f2a82 --- /dev/null +++ b/apps/web/src/app/tools/risk-insights/password-health-members.component.html @@ -0,0 +1,64 @@ +

    {{ "passwordsReportDesc" | i18n }}

    +
    + + {{ "loading" | i18n }} +
    +
    + + + + + {{ "name" | i18n }} + {{ "weakness" | i18n }} + {{ "timesReused" | i18n }} + {{ "timesExposed" | i18n }} + {{ "totalMembers" | i18n }} + + + + + + + + + + {{ r.name }} + +
    + {{ r.subTitle }} + + + + {{ passwordStrengthMap.get(r.id)[0] | i18n }} + + + + + {{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }} + + + + + {{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }} + + + + {{ totalMembersMap.get(r.id) || 0 }} + + +
    +
    +
    diff --git a/apps/web/src/app/tools/access-intelligence/password-health-members.component.ts b/apps/web/src/app/tools/risk-insights/password-health-members.component.ts similarity index 78% rename from apps/web/src/app/tools/access-intelligence/password-health-members.component.ts rename to apps/web/src/app/tools/risk-insights/password-health-members.component.ts index 17b2456406b..2581de78ed5 100644 --- a/apps/web/src/app/tools/access-intelligence/password-health-members.component.ts +++ b/apps/web/src/app/tools/risk-insights/password-health-members.component.ts @@ -5,13 +5,19 @@ import { ActivatedRoute } from "@angular/router"; import { debounceTime, map } from "rxjs"; // eslint-disable-next-line no-restricted-imports -import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence"; +import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { BadgeVariant, SearchModule, TableDataSource, TableModule } from "@bitwarden/components"; +import { + BadgeVariant, + SearchModule, + TableDataSource, + TableModule, + ToastService, +} from "@bitwarden/components"; import { CardComponent } from "@bitwarden/tools-card"; import { HeaderModule } from "../../layouts/header/header.module"; @@ -21,8 +27,6 @@ import { OrganizationBadgeModule } from "../../vault/individual-vault/organizati // eslint-disable-next-line no-restricted-imports import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; -import { NoPriorityAppsComponent } from "./no-priority-apps.component"; - @Component({ standalone: true, selector: "tools-password-health-members", @@ -34,7 +38,6 @@ import { NoPriorityAppsComponent } from "./no-priority-apps.component"; HeaderModule, SearchModule, FormsModule, - NoPriorityAppsComponent, SharedModule, TableModule, ], @@ -53,6 +56,8 @@ export class PasswordHealthMembersComponent implements OnInit { loading = true; + selectedIds: Set = new Set(); + protected searchControl = new FormControl("", { nonNullable: true }); private destroyRef = inject(DestroyRef); @@ -63,6 +68,7 @@ export class PasswordHealthMembersComponent implements OnInit { protected auditService: AuditService, protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, + protected toastService: ToastService, ) { this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) @@ -99,4 +105,32 @@ export class PasswordHealthMembersComponent implements OnInit { this.totalMembersMap = passwordHealthService.totalMembersMap; this.loading = false; } + + markAppsAsCritical = async () => { + // TODO: Send to API once implemented + return new Promise((resolve) => { + setTimeout(() => { + this.selectedIds.clear(); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("appsMarkedAsCritical"), + }); + resolve(true); + }, 1000); + }); + }; + + trackByFunction(_: number, item: CipherView) { + return item.id; + } + + onCheckboxChange(id: number, event: Event) { + const isChecked = (event.target as HTMLInputElement).checked; + if (isChecked) { + this.selectedIds.add(id); + } else { + this.selectedIds.delete(id); + } + } } diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.html b/apps/web/src/app/tools/risk-insights/password-health.component.html similarity index 100% rename from apps/web/src/app/tools/access-intelligence/password-health.component.html rename to apps/web/src/app/tools/risk-insights/password-health.component.html diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts b/apps/web/src/app/tools/risk-insights/password-health.component.spec.ts similarity index 98% rename from apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts rename to apps/web/src/app/tools/risk-insights/password-health.component.spec.ts index d41807e7d2d..50295b435b2 100644 --- a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts +++ b/apps/web/src/app/tools/risk-insights/password-health.component.spec.ts @@ -4,7 +4,7 @@ import { mock } from "jest-mock-extended"; import { of } from "rxjs"; // eslint-disable-next-line no-restricted-imports -import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence"; +import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.ts b/apps/web/src/app/tools/risk-insights/password-health.component.ts similarity index 98% rename from apps/web/src/app/tools/access-intelligence/password-health.component.ts rename to apps/web/src/app/tools/risk-insights/password-health.component.ts index 4b7b8e394d3..c3c1732854d 100644 --- a/apps/web/src/app/tools/access-intelligence/password-health.component.ts +++ b/apps/web/src/app/tools/risk-insights/password-health.component.ts @@ -6,7 +6,7 @@ import { map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; // eslint-disable-next-line no-restricted-imports -import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence"; +import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts b/apps/web/src/app/tools/risk-insights/risk-insights-routing.module.ts similarity index 70% rename from apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts rename to apps/web/src/app/tools/risk-insights/risk-insights-routing.module.ts index 88efb2b4832..19cc6f6832c 100644 --- a/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts +++ b/apps/web/src/app/tools/risk-insights/risk-insights-routing.module.ts @@ -4,15 +4,15 @@ import { RouterModule, Routes } from "@angular/router"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { AccessIntelligenceComponent } from "./access-intelligence.component"; +import { RiskInsightsComponent } from "./risk-insights.component"; const routes: Routes = [ { path: "", - component: AccessIntelligenceComponent, + component: RiskInsightsComponent, canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)], data: { - titleId: "accessIntelligence", + titleId: "RiskInsights", }, }, ]; @@ -21,4 +21,4 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) -export class AccessIntelligenceRoutingModule {} +export class RiskInsightsRoutingModule {} diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html b/apps/web/src/app/tools/risk-insights/risk-insights.component.html similarity index 67% rename from apps/web/src/app/tools/access-intelligence/access-intelligence.component.html rename to apps/web/src/app/tools/risk-insights/risk-insights.component.html index 44ca90cfaa5..c2cd0cac707 100644 --- a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html +++ b/apps/web/src/app/tools/risk-insights/risk-insights.component.html @@ -1,4 +1,4 @@ -
    {{ "accessIntelligence" | i18n }}
    +
    {{ "riskInsights" | i18n }}

    {{ "passwordRisk" | i18n }}

    {{ "discoverAtRiskPasswords" | i18n }}
    @@ -15,26 +15,27 @@ {{ "refresh" | i18n }}
    - + + + + + + + + {{ "criticalApplicationsWithCount" | i18n: criticalApps.length }} + + + - + + {{ "name" | i18n }} + + + + {{ "name" | i18n }} + {{ "owner" | i18n }} {{ "collections" | i18n }} - {{ "groups" | i18n }} - + + {{ "groups" | i18n }} + + {{ "permission" | i18n }} diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index ec91b135350..1c658663196 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -1,13 +1,17 @@ import { SelectionModel } from "@angular/cdk/collections"; import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { CollectionView, Unassigned } from "@bitwarden/admin-console/common"; +import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { TableDataSource } from "@bitwarden/components"; +import { SortDirection, TableDataSource } from "@bitwarden/components"; import { GroupView } from "../../../admin-console/organizations/core"; +import { + CollectionPermission, + convertToPermission, +} from "./../../../admin-console/organizations/shared/components/access-selector/access-selector.models"; import { VaultItem } from "./vault-item"; import { VaultItemEvent } from "./vault-item-event"; @@ -17,6 +21,8 @@ export const RowHeightClass = `tw-h-[75.5px]`; const MaxSelectionCount = 500; +type ItemPermission = CollectionPermission | "NoAccess"; + @Component({ selector: "app-vault-items", templateUrl: "vault-items.component.html", @@ -333,6 +339,119 @@ export class VaultItemsComponent { return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected; } + /** + * Sorts VaultItems, grouping collections before ciphers, and sorting each group alphabetically by name. + */ + protected sortByName = (a: VaultItem, b: VaultItem, direction: SortDirection) => { + // Collections before ciphers + const collectionCompare = this.prioritizeCollections(a, b, direction); + if (collectionCompare !== 0) { + return collectionCompare; + } + + return this.compareNames(a, b); + }; + + /** + * Sorts VaultItems based on group names + */ + protected sortByGroups = (a: VaultItem, b: VaultItem, direction: SortDirection) => { + if ( + !(a.collection instanceof CollectionAdminView) && + !(b.collection instanceof CollectionAdminView) + ) { + return 0; + } + + const getFirstGroupName = (collection: CollectionAdminView): string => { + if (collection.groups.length > 0) { + return collection.groups.map((group) => this.getGroupName(group.id) || "").sort()[0]; + } + return null; + }; + + // Collections before ciphers + const collectionCompare = this.prioritizeCollections(a, b, direction); + if (collectionCompare !== 0) { + return collectionCompare; + } + + const aGroupName = getFirstGroupName(a.collection as CollectionAdminView); + const bGroupName = getFirstGroupName(b.collection as CollectionAdminView); + + // Collections with groups come before collections without groups. + // If a collection has no groups, getFirstGroupName returns null. + if (aGroupName === null) { + return 1; + } + + if (bGroupName === null) { + return -1; + } + + return aGroupName.localeCompare(bGroupName); + }; + + /** + * Sorts VaultItems based on their permissions, with higher permissions taking precedence. + * If permissions are equal, it falls back to sorting by name. + */ + protected sortByPermissions = (a: VaultItem, b: VaultItem, direction: SortDirection) => { + const getPermissionPriority = (item: VaultItem): number => { + const permission = item.collection + ? this.getCollectionPermission(item.collection) + : this.getCipherPermission(item.cipher); + + const priorityMap = { + [CollectionPermission.Manage]: 5, + [CollectionPermission.Edit]: 4, + [CollectionPermission.EditExceptPass]: 3, + [CollectionPermission.View]: 2, + [CollectionPermission.ViewExceptPass]: 1, + NoAccess: 0, + }; + + return priorityMap[permission] ?? -1; + }; + + // Collections before ciphers + const collectionCompare = this.prioritizeCollections(a, b, direction); + if (collectionCompare !== 0) { + return collectionCompare; + } + + const priorityA = getPermissionPriority(a); + const priorityB = getPermissionPriority(b); + + // Higher priority first + if (priorityA !== priorityB) { + return priorityA - priorityB; + } + + return this.compareNames(a, b); + }; + + private compareNames(a: VaultItem, b: VaultItem): number { + const getName = (item: VaultItem) => item.collection?.name || item.cipher?.name; + return getName(a).localeCompare(getName(b)); + } + + /** + * Sorts VaultItems by prioritizing collections over ciphers. + * Collections are always placed before ciphers, regardless of the sorting direction. + */ + private prioritizeCollections(a: VaultItem, b: VaultItem, direction: SortDirection): number { + if (a.collection && !b.collection) { + return direction === "asc" ? -1 : 1; + } + + if (!a.collection && b.collection) { + return direction === "asc" ? 1 : -1; + } + + return 0; + } + private hasPersonalItems(): boolean { return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null); } @@ -346,4 +465,58 @@ export class VaultItemsComponent { private getUniqueOrganizationIds(): Set { return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? [])); } + + private getGroupName(groupId: string): string | undefined { + return this.allGroups.find((g) => g.id === groupId)?.name; + } + + private getCollectionPermission(collection: CollectionView): ItemPermission { + const organization = this.allOrganizations.find((o) => o.id === collection.organizationId); + + if (collection.id == Unassigned && organization?.canEditUnassignedCiphers) { + return CollectionPermission.Edit; + } + + if (collection.assigned) { + return convertToPermission(collection); + } + + return "NoAccess"; + } + + private getCipherPermission(cipher: CipherView): ItemPermission { + if (!cipher.organizationId || cipher.collectionIds.length === 0) { + return CollectionPermission.Manage; + } + + const filteredCollections = this.allCollections?.filter((collection) => { + if (collection.assigned) { + return cipher.collectionIds.find((id) => { + if (collection.id === id) { + return collection; + } + }); + } + }); + + if (filteredCollections?.length === 1) { + return convertToPermission(filteredCollections[0]); + } + + if (filteredCollections?.length > 0) { + const permissions = filteredCollections.map((collection) => convertToPermission(collection)); + + const orderedPermissions = [ + CollectionPermission.Manage, + CollectionPermission.Edit, + CollectionPermission.EditExceptPass, + CollectionPermission.View, + CollectionPermission.ViewExceptPass, + ]; + + return orderedPermissions.find((perm) => permissions.includes(perm)); + } + + return "NoAccess"; + } } diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.html b/apps/web/src/app/vault/individual-vault/add-edit.component.html index 855b5dac489..01ac60fc7e6 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.html +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.html @@ -851,6 +851,99 @@
    + + +
    +
    + +
    + +
    + + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    +